1use alloc::{
9 boxed::Box,
10 collections::{btree_map::BTreeMap, VecDeque},
11 sync::Arc,
12 vec::Vec,
13};
14
15#[cfg(feature = "std")]
16use std::sync::Mutex;
17
18use azul_core::{
19 resources::UpdateImageType,
20 callbacks::{CoreCallback, FocusTarget, FocusTargetPath, HidpiAdjustedBounds, Update},
21 dom::{DomId, DomIdVec, DomNodeId, IdOrClass, NodeId, NodeType},
22 geom::{LogicalPosition, LogicalRect, LogicalSize, OptionLogicalPosition, OptionCursorNodePosition, OptionScreenPosition, OptionDragDelta, CursorNodePosition, ScreenPosition, DragDelta},
23 gl::OptionGlContextPtr,
24 gpu::GpuValueCache,
25 hit_test::ScrollPosition,
26 id::NodeId as CoreNodeId,
27 impl_callback,
28 menu::Menu,
29 refany::{OptionRefAny, RefAny},
30 resources::{ImageCache, ImageMask, ImageRef, LoadedFont, LoadedFontVec, RendererResources},
31 selection::{Selection, SelectionRange, SelectionRangeVec, SelectionState, TextCursor},
32 styled_dom::{NodeHierarchyItemId, NodeHierarchyItemIdVec, StyledDom},
33 task::{self, GetSystemTimeCallback, Instant, ThreadId, ThreadIdVec, TimerId, TimerIdVec},
34 window::{KeyboardState, Monitor, MonitorVec, MouseState, OptionMonitor, RawWindowHandle, WindowFlags, WindowSize},
35 FastBTreeSet, OrderedMap,
36};
37use azul_css::{
38 css::CssPath,
39 props::{
40 basic::FontRef,
41 property::{CssProperty, CssPropertyType, CssPropertyVec},
42 },
43 system::SystemStyle,
44 corety::{OptionString, OptionUsize},
45 AzString, OptionU8Vec, StringVec, U8Vec,
46};
47use rust_fontconfig::FcFontCache;
48
49#[cfg(feature = "icu")]
50use crate::icu::{
51 FormatLength, IcuDate, IcuDateTime, IcuLocalizerHandle, IcuResult,
52 IcuStringVec, IcuTime, ListType, PluralCategory,
53};
54
55use crate::{
56 hit_test::FullHitTest,
57 managers::{
58 file_drop::FileDropManager,
59 focus_cursor::FocusManager,
60 gesture::{GestureAndDragManager, InputSample, PenState},
61 gpu_state::GpuStateManager,
62 hover::{HoverManager, InputPointId},
63 virtual_view::VirtualViewManager,
64 scroll_state::{AnimatedScrollState, ScrollManager},
65 selection::ClipboardContent,
66 text_input::{PendingTextEdit, TextInputManager},
67 undo_redo::{UndoRedoManager, UndoableOperation},
68 },
69 text3::cache::{TextShapingCache as TextLayoutCache, UnifiedLayout},
70 thread::{CreateThreadCallback, Thread},
71 timer::Timer,
72 window::{DomLayoutResult, LayoutWindow},
73 window_state::{FullWindowState, FullWindowStateVec, WindowCreateOptions},
74};
75
76use azul_css::{impl_option, impl_option_inner};
77
78#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
84#[repr(C)]
85pub struct PenTilt {
86 pub x_tilt: f32,
88 pub y_tilt: f32,
90}
91
92impl From<(f32, f32)> for PenTilt {
93 fn from((x, y): (f32, f32)) -> Self {
94 Self {
95 x_tilt: x,
96 y_tilt: y,
97 }
98 }
99}
100
101impl_option!(
102 PenTilt,
103 OptionPenTilt,
104 [Debug, Clone, Copy, PartialEq, PartialOrd]
105);
106
107#[derive(Debug, Clone, PartialEq, Eq)]
109#[repr(C)]
110pub struct SelectAllResult {
111 pub full_text: AzString,
113 pub selection_range: SelectionRange,
115}
116
117impl From<(String, SelectionRange)> for SelectAllResult {
118 fn from((text, range): (String, SelectionRange)) -> Self {
119 Self {
120 full_text: text.into(),
121 selection_range: range,
122 }
123 }
124}
125
126impl_option!(
127 SelectAllResult,
128 OptionSelectAllResult,
129 copy = false,
130 [Debug, Clone, PartialEq, Eq]
131);
132
133#[derive(Debug, Clone, PartialEq, Eq)]
135#[repr(C)]
136pub struct DeleteResult {
137 pub range_to_delete: SelectionRange,
139 pub deleted_text: AzString,
141}
142
143impl From<(SelectionRange, String)> for DeleteResult {
144 fn from((range, text): (SelectionRange, String)) -> Self {
145 Self {
146 range_to_delete: range,
147 deleted_text: text.into(),
148 }
149 }
150}
151
152impl_option!(
153 DeleteResult,
154 OptionDeleteResult,
155 copy = false,
156 [Debug, Clone, PartialEq, Eq]
157);
158
159#[derive(Debug, Clone)]
167pub enum CallbackChange {
168 ModifyWindowState { state: FullWindowState },
171 InjectNativeGesture {
176 gesture: crate::managers::gesture::NativeGestureEvent,
177 },
178 QueueWindowStateSequence { states: Vec<FullWindowState> },
182 CreateNewWindow { options: WindowCreateOptions },
184 CloseWindow,
186
187 SetFocusTarget { target: FocusTarget },
190
191 StopPropagation,
196 StopImmediatePropagation,
199 PreventDefault,
201
202 AddTimer { timer_id: TimerId, timer: Timer },
205 RemoveTimer { timer_id: TimerId },
207
208 AddThread { thread_id: ThreadId, thread: Thread },
211 RemoveThread { thread_id: ThreadId },
213
214 ChangeNodeText { node_id: DomNodeId, text: AzString },
217 ChangeNodeImage {
219 dom_id: DomId,
220 node_id: NodeId,
221 image: ImageRef,
222 update_type: UpdateImageType,
223 },
224 UpdateImageCallback { dom_id: DomId, node_id: NodeId },
227 UpdateAllImageCallbacks,
234 UpdateVirtualView { dom_id: DomId, node_id: NodeId },
237 UpdateAllVirtualViews,
241 ChangeNodeImageMask {
243 dom_id: DomId,
244 node_id: NodeId,
245 mask: ImageMask,
246 },
247 ChangeNodeCssProperties {
249 dom_id: DomId,
250 node_id: NodeId,
251 properties: CssPropertyVec,
252 },
253 OverrideNodeCssProperties {
261 dom_id: DomId,
262 node_id: NodeId,
263 properties: CssPropertyVec,
264 },
265
266 ScrollTo {
269 dom_id: DomId,
270 node_id: NodeHierarchyItemId,
271 position: LogicalPosition,
272 unclamped: bool,
275 },
276 ScrollIntoView {
279 node_id: DomNodeId,
280 options: crate::managers::scroll_into_view::ScrollIntoViewOptions,
281 },
282
283 AddImageToCache { id: AzString, image: ImageRef },
286 RemoveImageFromCache { id: AzString },
288
289 ReloadSystemFonts,
292
293 OpenMenu {
297 menu: Menu,
298 position: Option<LogicalPosition>,
300 },
301
302 ShowTooltip {
311 text: AzString,
312 position: LogicalPosition,
313 },
314 HideTooltip,
316
317 InsertText {
320 dom_id: DomId,
321 node_id: NodeId,
322 text: AzString,
323 },
324 DeleteBackward { dom_id: DomId, node_id: NodeId },
326 DeleteForward { dom_id: DomId, node_id: NodeId },
328 MoveCursor {
330 dom_id: DomId,
331 node_id: NodeId,
332 cursor: TextCursor,
333 },
334 SetSelection {
336 dom_id: DomId,
337 node_id: NodeId,
338 selection: Selection,
339 },
340 SetTextChangeset { changeset: PendingTextEdit },
343
344 MoveCursorLeft {
347 dom_id: DomId,
348 node_id: NodeId,
349 extend_selection: bool,
350 },
351 MoveCursorRight {
353 dom_id: DomId,
354 node_id: NodeId,
355 extend_selection: bool,
356 },
357 MoveCursorUp {
359 dom_id: DomId,
360 node_id: NodeId,
361 extend_selection: bool,
362 },
363 MoveCursorDown {
365 dom_id: DomId,
366 node_id: NodeId,
367 extend_selection: bool,
368 },
369 MoveCursorToLineStart {
371 dom_id: DomId,
372 node_id: NodeId,
373 extend_selection: bool,
374 },
375 MoveCursorToLineEnd {
377 dom_id: DomId,
378 node_id: NodeId,
379 extend_selection: bool,
380 },
381 MoveCursorToDocumentStart {
383 dom_id: DomId,
384 node_id: NodeId,
385 extend_selection: bool,
386 },
387 MoveCursorToDocumentEnd {
389 dom_id: DomId,
390 node_id: NodeId,
391 extend_selection: bool,
392 },
393
394 AddCursor {
397 dom_id: DomId,
398 node_id: NodeId,
399 cursor: TextCursor,
400 },
401 AddSelectionRange {
403 dom_id: DomId,
404 node_id: NodeId,
405 range: SelectionRange,
406 },
407 RemoveSelectionById {
409 selection_id: azul_core::selection::SelectionId,
410 },
411
412 SetCopyContent {
415 target: DomNodeId,
416 content: ClipboardContent,
417 },
418 SetCutContent {
420 target: DomNodeId,
421 content: ClipboardContent,
422 },
423 SetSelectAllRange {
425 target: DomNodeId,
426 range: SelectionRange,
427 },
428
429 RequestHitTestUpdate { position: LogicalPosition },
436
437 ProcessTextSelectionClick {
446 position: LogicalPosition,
447 time_ms: u64,
448 },
449
450 SetCursorVisibility { visible: bool },
453 ToggleCursorVisibility,
455 ResetCursorBlink,
457 StartCursorBlinkTimer,
459 StopCursorBlinkTimer,
461
462 ScrollActiveCursorIntoView,
466
467 CreateTextInput {
477 text: AzString,
479 },
480
481 BeginInteractiveMove,
486
487 SetDragData {
491 mime_type: AzString,
492 data: Vec<u8>,
493 },
494 AcceptDrop,
497 SetDropEffect {
499 effect: azul_core::drag::DropEffect,
500 },
501
502 InsertChildNode {
508 dom_id: DomId,
509 parent_node_id: NodeId,
510 node_type_str: AzString,
512 position: Option<usize>,
514 classes: Vec<AzString>,
516 id: Option<AzString>,
518 },
519 DeleteNode {
523 dom_id: DomId,
524 node_id: NodeId,
525 },
526 SetNodeIdsAndClasses {
528 dom_id: DomId,
529 node_id: NodeId,
530 ids_and_classes: azul_core::dom::IdOrClassVec,
531 },
532
533 SwitchRoute {
540 pattern: AzString,
542 params: azul_core::window::StringPairVec,
544 },
545
546 CommitUndoSnapshot,
549 UndoAppState,
551 RedoAppState,
553}
554
555pub type CallbackType = extern "C" fn(RefAny, CallbackInfo) -> Update;
557
558#[repr(C)]
562pub struct Callback {
563 pub cb: CallbackType,
564 pub ctx: OptionRefAny,
567}
568
569impl_callback!(Callback, CallbackType);
570
571azul_core::impl_managed_callback! {
578 wrapper: Callback,
579 info_ty: CallbackInfo,
580 return_ty: Update,
581 default_ret: Update::DoNothing,
582 invoker_static: CALLBACK_INVOKER,
583 invoker_ty: AzCallbackInvoker,
584 thunk_fn: az_callback_thunk,
585 setter_fn: AzApp_setCallbackInvoker,
586 from_handle_fn: AzCallback_createFromHostHandle,
587}
588
589impl Callback {
590 #[must_use]
597 pub fn from_ptr(cb: CallbackType) -> Self {
598 Self::from(cb)
599 }
600
601 pub fn create<C: Into<Self>>(cb: C) -> Self {
603 cb.into()
604 }
605
606 #[must_use] pub fn from_core(core: CoreCallback) -> Self {
620 debug_assert!(core.cb != 0, "CoreCallback.cb is null");
621 Self {
622 cb: unsafe { core::mem::transmute::<usize, CallbackType>(core.cb) },
623 ctx: core.ctx,
624 }
625 }
626
627 #[must_use] pub fn to_core(self) -> CoreCallback {
631 CoreCallback {
632 cb: self.cb as usize,
633 ctx: self.ctx,
634 }
635 }
636}
637
638impl From<Callback> for CoreCallback {
640 fn from(callback: Callback) -> Self {
641 callback.to_core()
642 }
643}
644
645impl Callback {
646 #[must_use] pub fn invoke(&self, data: RefAny, info: CallbackInfo) -> Update {
650 (self.cb)(data, info)
651 }
652}
653#[allow(variant_size_differences)] #[derive(Debug, Eq, Clone, PartialEq, PartialOrd, Ord, Hash)]
659#[repr(C, u8)]
660pub enum OptionCallback {
661 None,
663 Some(Callback),
665}
666
667impl OptionCallback {
668 #[must_use] pub fn into_option(self) -> Option<Callback> {
670 match self {
671 Self::None => None,
672 Self::Some(c) => Some(c),
673 }
674 }
675
676 #[must_use] pub const fn is_some(&self) -> bool {
678 matches!(self, Self::Some(_))
679 }
680
681 #[must_use] pub const fn is_none(&self) -> bool {
683 matches!(self, Self::None)
684 }
685}
686
687impl From<Option<Callback>> for OptionCallback {
688 fn from(o: Option<Callback>) -> Self {
689 o.map_or_else(|| Self::None, Self::Some)
690 }
691}
692
693impl From<OptionCallback> for Option<Callback> {
694 fn from(o: OptionCallback) -> Self {
695 o.into_option()
696 }
697}
698
699#[derive(Debug)]
708pub struct CallbackInfoRefData<'a> {
709 pub layout_window: &'a LayoutWindow,
711 pub renderer_resources: &'a RendererResources,
713 pub previous_window_state: &'a Option<FullWindowState>,
715 pub current_window_state: &'a FullWindowState,
717 pub gl_context: &'a OptionGlContextPtr,
719 pub current_scroll_manager: &'a BTreeMap<DomId, BTreeMap<NodeHierarchyItemId, ScrollPosition>>,
721 pub current_window_handle: &'a RawWindowHandle,
723 pub system_callbacks: &'a ExternalSystemCallbacks,
725 pub system_style: Arc<SystemStyle>,
728 pub monitors: Arc<Mutex<MonitorVec>>,
732 #[cfg(feature = "icu")]
735 pub icu_localizer: IcuLocalizerHandle,
736 pub ctx: OptionRefAny,
739}
740
741#[derive(Debug, Clone, Copy)]
764#[repr(C)]
765pub struct CallbackInfo {
766 ref_data: *const CallbackInfoRefData<'static>,
770 hit_dom_node: DomNodeId,
773 cursor_relative_to_item: OptionLogicalPosition,
776 cursor_in_viewport: OptionLogicalPosition,
778 #[cfg(feature = "std")]
782 changes: *const Arc<Mutex<Vec<CallbackChange>>>,
783 #[cfg(not(feature = "std"))]
784 changes: *mut Vec<CallbackChange>,
785}
786
787impl CallbackInfo {
788 #[cfg(feature = "std")]
789 pub const fn new<'a>(
790 ref_data: &'a CallbackInfoRefData<'a>,
791 changes: &'a Arc<Mutex<Vec<CallbackChange>>>,
792 hit_dom_node: DomNodeId,
793 cursor_relative_to_item: OptionLogicalPosition,
794 cursor_in_viewport: OptionLogicalPosition,
795 ) -> Self {
796 Self {
797 ref_data: std::ptr::from_ref::<CallbackInfoRefData<'a>>(ref_data).cast::<CallbackInfoRefData<'static>>(),
803
804 hit_dom_node,
806 cursor_relative_to_item,
807 cursor_in_viewport,
808
809 changes: std::ptr::from_ref::<Arc<Mutex<Vec<CallbackChange>>>>(changes),
811 }
812 }
813
814 #[cfg(not(feature = "std"))]
815 pub fn new<'a>(
816 ref_data: &'a CallbackInfoRefData<'a>,
817 changes: &'a mut Vec<CallbackChange>,
818 hit_dom_node: DomNodeId,
819 cursor_relative_to_item: OptionLogicalPosition,
820 cursor_in_viewport: OptionLogicalPosition,
821 ) -> Self {
822 Self {
823 ref_data: ref_data as *const CallbackInfoRefData<'a> as *const CallbackInfoRefData<'static>,
825 hit_dom_node,
826 cursor_relative_to_item,
827 cursor_in_viewport,
828 changes: changes as *mut Vec<CallbackChange>,
829 }
830 }
831
832 #[must_use] pub fn get_ctx(&self) -> OptionRefAny {
837 unsafe { (*self.ref_data).ctx.clone() }
838 }
839
840 #[must_use] pub fn get_gl_context(&self) -> OptionGlContextPtr {
842 unsafe { (*self.ref_data).gl_context.clone() }
843 }
844
845 #[cfg(feature = "std")]
850 pub fn push_change(&mut self, change: CallbackChange) {
851 unsafe {
853 if let Ok(mut changes) = (*self.changes).lock() {
854 changes.push(change);
855 }
856 }
857 }
858
859 #[cfg(not(feature = "std"))]
860 pub fn push_change(&mut self, change: CallbackChange) {
861 unsafe { (*self.changes).push(change) }
862 }
863
864 pub fn commit_undo_snapshot(&mut self) {
866 self.push_change(CallbackChange::CommitUndoSnapshot);
867 }
868
869 pub fn undo_app_state(&mut self) {
871 self.push_change(CallbackChange::UndoAppState);
872 }
873
874 pub fn redo_app_state(&mut self) {
876 self.push_change(CallbackChange::RedoAppState);
877 }
878
879 #[cfg(feature = "std")]
881 #[must_use] pub const fn get_changes_ptr(&self) -> *const () {
882 self.changes.cast::<()>()
883 }
884
885 #[cfg(feature = "std")]
887 #[must_use] pub fn take_changes(&self) -> Vec<CallbackChange> {
888 unsafe {
890 (*self.changes).lock().map_or_else(
891 |_| Vec::new(),
892 |mut changes| core::mem::take(&mut *changes),
893 )
894 }
895 }
896
897 #[cfg(not(feature = "std"))]
898 pub fn take_changes(&self) -> Vec<CallbackChange> {
899 unsafe { core::mem::take(&mut *self.changes) }
900 }
901
902 #[cfg(feature = "std")]
910 #[must_use] pub fn has_pending_relayout_change(&self) -> bool {
911 unsafe {
912 (*self.changes).lock().is_ok_and(|changes| changes.iter().any(|c| matches!(c,
913 CallbackChange::ModifyWindowState { .. } |
914 CallbackChange::ScrollTo { .. } |
915 CallbackChange::QueueWindowStateSequence { .. }
920 )))
921 }
922 }
923
924 pub fn add_timer(&mut self, timer_id: TimerId, timer: Timer) {
928 self.push_change(CallbackChange::AddTimer { timer_id, timer });
929 }
930
931 pub fn remove_timer(&mut self, timer_id: TimerId) {
933 self.push_change(CallbackChange::RemoveTimer { timer_id });
934 }
935
936 pub fn add_thread(&mut self, thread_id: ThreadId, thread: Thread) {
938 self.push_change(CallbackChange::AddThread { thread_id, thread });
939 }
940
941 pub fn remove_thread(&mut self, thread_id: ThreadId) {
943 self.push_change(CallbackChange::RemoveThread { thread_id });
944 }
945
946 pub fn stop_propagation(&mut self) {
951 self.push_change(CallbackChange::StopPropagation);
952 }
953
954 pub fn stop_immediate_propagation(&mut self) {
959 self.push_change(CallbackChange::StopImmediatePropagation);
960 }
961
962 pub fn set_focus(&mut self, target: FocusTarget) {
964 self.push_change(CallbackChange::SetFocusTarget { target });
965 }
966
967 pub fn create_window(&mut self, options: WindowCreateOptions) {
969 self.push_change(CallbackChange::CreateNewWindow { options });
970 }
971
972 pub fn close_window(&mut self) {
974 self.push_change(CallbackChange::CloseWindow);
975 }
976
977 pub fn switch_route(&mut self, pattern: AzString, params: azul_core::window::StringPairVec) {
988 self.push_change(CallbackChange::SwitchRoute { pattern, params });
989 }
990
991 #[must_use] pub fn get_route_pattern(&self) -> AzString {
1000 match &self.get_current_window_state().active_route {
1001 azul_core::resources::OptionRouteMatch::Some(rm) => rm.pattern.clone(),
1002 azul_core::resources::OptionRouteMatch::None => AzString::from_const_str(""),
1003 }
1004 }
1005
1006 #[allow(clippy::needless_pass_by_value)]
1016 #[must_use] pub fn get_route_param(&self, key: AzString) -> AzString {
1017 match &self.get_current_window_state().active_route {
1018 azul_core::resources::OptionRouteMatch::Some(rm) => {
1019 rm.get_param(key.as_str())
1020 .cloned()
1021 .unwrap_or_else(|| AzString::from_const_str(""))
1022 }
1023 azul_core::resources::OptionRouteMatch::None => AzString::from_const_str(""),
1024 }
1025 }
1026
1027 pub fn set_route_param(&mut self, key: AzString, value: AzString) {
1037 let ws = self.get_current_window_state();
1038 let pattern = match &ws.active_route {
1039 azul_core::resources::OptionRouteMatch::Some(rm) => rm.pattern.clone(),
1040 azul_core::resources::OptionRouteMatch::None => return,
1041 };
1042 let mut params = match &ws.active_route {
1043 azul_core::resources::OptionRouteMatch::Some(rm) => {
1044 rm.params.as_ref().to_vec()
1045 }
1046 azul_core::resources::OptionRouteMatch::None => return,
1047 };
1048 if let Some(existing) = params.iter_mut().find(|p| p.key.as_str() == key.as_str()) {
1050 existing.value = value;
1051 } else {
1052 params.push(azul_core::window::AzStringPair { key, value });
1053 }
1054 self.push_change(CallbackChange::SwitchRoute {
1055 pattern,
1056 params: azul_core::window::StringPairVec::from_vec(params),
1057 });
1058 }
1059
1060 pub fn modify_window_state(&mut self, state: FullWindowState) {
1062 self.push_change(CallbackChange::ModifyWindowState { state });
1063 }
1064
1065 pub fn begin_interactive_move(&mut self) {
1071 self.push_change(CallbackChange::BeginInteractiveMove);
1072 }
1073
1074 pub fn queue_window_state_sequence(&mut self, states: FullWindowStateVec) {
1078 self.push_change(CallbackChange::QueueWindowStateSequence {
1079 states: states.into_library_owned_vec(),
1080 });
1081 }
1082
1083 pub fn change_node_text(&mut self, node_id: DomNodeId, text: AzString) {
1091 self.push_change(CallbackChange::ChangeNodeText { node_id, text });
1092 }
1093
1094 pub fn change_node_image(
1096 &mut self,
1097 dom_id: DomId,
1098 node_id: NodeId,
1099 image: ImageRef,
1100 update_type: UpdateImageType,
1101 ) {
1102 self.push_change(CallbackChange::ChangeNodeImage {
1103 dom_id,
1104 node_id,
1105 image,
1106 update_type,
1107 });
1108 }
1109
1110 pub fn update_image_callback(&mut self, dom_id: DomId, node_id: NodeId) {
1118 self.push_change(CallbackChange::UpdateImageCallback { dom_id, node_id });
1119 }
1120
1121 pub fn update_all_image_callbacks(&mut self) {
1135 self.push_change(CallbackChange::UpdateAllImageCallbacks);
1136 }
1137
1138 pub fn trigger_virtual_view_rerender(&mut self, dom_id: DomId, node_id: NodeId) {
1149 self.push_change(CallbackChange::UpdateVirtualView { dom_id, node_id });
1150 }
1151
1152 pub fn trigger_all_virtual_view_rerender(&mut self) {
1163 self.push_change(CallbackChange::UpdateAllVirtualViews);
1164 }
1165
1166 #[must_use] pub fn get_node_id_by_id_attribute(&self, dom_id: DomId, id: &str) -> Option<NodeId> {
1172 let layout_window = self.get_layout_window();
1173 let layout_result = layout_window.layout_results.get(&dom_id)?;
1174 let styled_dom = &layout_result.styled_dom;
1175
1176 for (node_idx, node_data) in styled_dom.node_data.as_ref().iter().enumerate() {
1178 if node_data.has_id(id) {
1179 return Some(NodeId::new(node_idx));
1180 }
1181 }
1182
1183 None
1184 }
1185
1186 #[must_use] pub fn get_parent_node(&self, dom_id: DomId, node_id: NodeId) -> Option<NodeId> {
1190 let layout_window = self.get_layout_window();
1191 let layout_result = layout_window.layout_results.get(&dom_id)?;
1192 let node_hierarchy = &layout_result.styled_dom.node_hierarchy;
1193 let node = node_hierarchy.as_ref().get(node_id.index())?;
1194 node.parent_id()
1195 }
1196
1197 #[must_use] pub fn get_next_sibling_node(&self, dom_id: DomId, node_id: NodeId) -> Option<NodeId> {
1201 let layout_window = self.get_layout_window();
1202 let layout_result = layout_window.layout_results.get(&dom_id)?;
1203 let node_hierarchy = &layout_result.styled_dom.node_hierarchy;
1204 let node = node_hierarchy.as_ref().get(node_id.index())?;
1205 node.next_sibling_id()
1206 }
1207
1208 #[must_use] pub fn get_previous_sibling_node(&self, dom_id: DomId, node_id: NodeId) -> Option<NodeId> {
1212 let layout_window = self.get_layout_window();
1213 let layout_result = layout_window.layout_results.get(&dom_id)?;
1214 let node_hierarchy = &layout_result.styled_dom.node_hierarchy;
1215 let node = node_hierarchy.as_ref().get(node_id.index())?;
1216 node.previous_sibling_id()
1217 }
1218
1219 #[must_use] pub fn get_first_child_node(&self, dom_id: DomId, node_id: NodeId) -> Option<NodeId> {
1223 let layout_window = self.get_layout_window();
1224 let layout_result = layout_window.layout_results.get(&dom_id)?;
1225 let node_hierarchy = &layout_result.styled_dom.node_hierarchy;
1226 let node = node_hierarchy.as_ref().get(node_id.index())?;
1227 node.first_child_id(node_id)
1228 }
1229
1230 #[must_use] pub fn get_last_child_node(&self, dom_id: DomId, node_id: NodeId) -> Option<NodeId> {
1234 let layout_window = self.get_layout_window();
1235 let layout_result = layout_window.layout_results.get(&dom_id)?;
1236 let node_hierarchy = &layout_result.styled_dom.node_hierarchy;
1237 let node = node_hierarchy.as_ref().get(node_id.index())?;
1238 node.last_child_id()
1239 }
1240
1241 #[must_use] pub fn get_all_children_nodes(&self, dom_id: DomId, node_id: NodeId) -> NodeHierarchyItemIdVec {
1246 let layout_window = self.get_layout_window();
1247 let Some(layout_result) = layout_window.layout_results.get(&dom_id) else {
1248 return NodeHierarchyItemIdVec::from_const_slice(&[]);
1249 };
1250 let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
1251 let Some(hier_item) = node_hierarchy.get(node_id) else {
1252 return NodeHierarchyItemIdVec::from_const_slice(&[]);
1253 };
1254
1255 let Some(first_child) = hier_item.first_child_id(node_id) else {
1257 return NodeHierarchyItemIdVec::from_const_slice(&[]);
1258 };
1259
1260 let mut children: Vec<NodeHierarchyItemId> = Vec::new();
1262 children.push(NodeHierarchyItemId::from_crate_internal(Some(first_child)));
1263
1264 let mut current = first_child;
1265 while let Some(next_sibling) = node_hierarchy
1266 .get(current)
1267 .and_then(azul_core::styled_dom::NodeHierarchyItem::next_sibling_id)
1268 {
1269 children.push(NodeHierarchyItemId::from_crate_internal(Some(next_sibling)));
1270 current = next_sibling;
1271 }
1272
1273 NodeHierarchyItemIdVec::from(children)
1274 }
1275
1276 #[must_use] pub fn get_children_count(&self, dom_id: DomId, node_id: NodeId) -> usize {
1280 let layout_window = self.get_layout_window();
1281 let Some(layout_result) = layout_window.layout_results.get(&dom_id) else {
1282 return 0;
1283 };
1284 let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
1285 let Some(hier_item) = node_hierarchy.get(node_id) else {
1286 return 0;
1287 };
1288
1289 let Some(first_child) = hier_item.first_child_id(node_id) else {
1291 return 0;
1292 };
1293
1294 let mut count = 1;
1296 let mut current = first_child;
1297 while let Some(next_sibling) = node_hierarchy
1298 .get(current)
1299 .and_then(azul_core::styled_dom::NodeHierarchyItem::next_sibling_id)
1300 {
1301 count += 1;
1302 current = next_sibling;
1303 }
1304
1305 count
1306 }
1307
1308 pub fn change_node_image_mask(&mut self, dom_id: DomId, node_id: NodeId, mask: ImageMask) {
1310 self.push_change(CallbackChange::ChangeNodeImageMask {
1311 dom_id,
1312 node_id,
1313 mask,
1314 });
1315 }
1316
1317 pub fn change_node_css_properties(
1319 &mut self,
1320 dom_id: DomId,
1321 node_id: NodeId,
1322 properties: CssPropertyVec,
1323 ) {
1324 self.push_change(CallbackChange::ChangeNodeCssProperties {
1325 dom_id,
1326 node_id,
1327 properties,
1328 });
1329 }
1330
1331 pub fn set_css_property(&mut self, node_id: DomNodeId, property: CssProperty) {
1343 let dom_id = node_id.dom;
1344 let internal_node_id = node_id
1345 .node
1346 .into_crate_internal()
1347 .expect("DomNodeId node should not be None");
1348 self.change_node_css_properties(dom_id, internal_node_id, vec![property].into());
1349 }
1350
1351 pub fn override_node_css_properties(
1358 &mut self,
1359 dom_id: DomId,
1360 node_id: NodeId,
1361 properties: CssPropertyVec,
1362 ) {
1363 self.push_change(CallbackChange::OverrideNodeCssProperties {
1364 dom_id,
1365 node_id,
1366 properties,
1367 });
1368 }
1369
1370 pub fn override_css_property(&mut self, node_id: DomNodeId, property: CssProperty) {
1376 let dom_id = node_id.dom;
1377 let internal_node_id = node_id
1378 .node
1379 .into_crate_internal()
1380 .expect("DomNodeId node should not be None");
1381 self.override_node_css_properties(dom_id, internal_node_id, vec![property].into());
1382 }
1383
1384 pub fn scroll_to(
1386 &mut self,
1387 dom_id: DomId,
1388 node_id: NodeHierarchyItemId,
1389 position: LogicalPosition,
1390 ) {
1391 self.push_change(CallbackChange::ScrollTo {
1392 dom_id,
1393 node_id,
1394 position,
1395 unclamped: false,
1396 });
1397 }
1398
1399 pub fn scroll_to_unclamped(
1402 &mut self,
1403 dom_id: DomId,
1404 node_id: NodeHierarchyItemId,
1405 position: LogicalPosition,
1406 ) {
1407 self.push_change(CallbackChange::ScrollTo {
1408 dom_id,
1409 node_id,
1410 position,
1411 unclamped: true,
1412 });
1413 }
1414
1415 pub fn scroll_node_into_view(
1431 &mut self,
1432 node_id: DomNodeId,
1433 options: crate::managers::scroll_into_view::ScrollIntoViewOptions,
1434 ) {
1435 self.push_change(CallbackChange::ScrollIntoView {
1436 node_id,
1437 options,
1438 });
1439 }
1440
1441 pub fn add_image_to_cache(&mut self, id: AzString, image: ImageRef) {
1443 self.push_change(CallbackChange::AddImageToCache { id, image });
1444 }
1445
1446 pub fn remove_image_from_cache(&mut self, id: AzString) {
1448 self.push_change(CallbackChange::RemoveImageFromCache { id });
1449 }
1450
1451 pub fn reload_system_fonts(&mut self) {
1455 self.push_change(CallbackChange::ReloadSystemFonts);
1456 }
1457
1458 #[must_use] pub const fn get_text_changeset(&self) -> Option<&PendingTextEdit> {
1468 self.get_layout_window()
1469 .text_input_manager
1470 .get_pending_changeset()
1471 }
1472
1473 pub fn set_text_changeset(&mut self, changeset: PendingTextEdit) {
1481 self.push_change(CallbackChange::SetTextChangeset { changeset });
1482 }
1483
1484 pub fn create_text_input(&mut self, text: AzString) {
1500 self.push_change(CallbackChange::CreateTextInput { text });
1501 }
1502
1503 pub fn insert_child_node(
1520 &mut self,
1521 dom_id: DomId,
1522 parent_node_id: NodeId,
1523 node_type_str: AzString,
1524 position: OptionUsize,
1525 classes: StringVec,
1526 id: OptionString,
1527 ) {
1528 self.push_change(CallbackChange::InsertChildNode {
1529 dom_id,
1530 parent_node_id,
1531 node_type_str,
1532 position: position.into(),
1533 classes: classes.into_library_owned_vec(),
1534 id: id.into(),
1535 });
1536 }
1537
1538 pub fn delete_node(&mut self, dom_id: DomId, node_id: NodeId) {
1548 self.push_change(CallbackChange::DeleteNode { dom_id, node_id });
1549 }
1550
1551 pub fn set_node_ids_and_classes(
1560 &mut self,
1561 dom_id: DomId,
1562 node_id: NodeId,
1563 ids_and_classes: azul_core::dom::IdOrClassVec,
1564 ) {
1565 self.push_change(CallbackChange::SetNodeIdsAndClasses {
1566 dom_id,
1567 node_id,
1568 ids_and_classes,
1569 });
1570 }
1571
1572 pub fn prevent_default(&mut self) {
1577 self.push_change(CallbackChange::PreventDefault);
1578 }
1579
1580 pub fn set_cursor_visibility(&mut self, visible: bool) {
1587 self.push_change(CallbackChange::SetCursorVisibility { visible });
1588 }
1589
1590 pub fn reset_cursor_blink(&mut self) {
1596 self.push_change(CallbackChange::ResetCursorBlink);
1597 }
1598
1599 pub fn start_cursor_blink_timer(&mut self) {
1604 self.push_change(CallbackChange::StartCursorBlinkTimer);
1605 }
1606
1607 pub fn stop_cursor_blink_timer(&mut self) {
1611 self.push_change(CallbackChange::StopCursorBlinkTimer);
1612 }
1613
1614 pub fn scroll_active_cursor_into_view(&mut self) {
1619 self.push_change(CallbackChange::ScrollActiveCursorIntoView);
1620 }
1621
1622 pub fn open_menu(&mut self, menu: Menu) {
1631 self.push_change(CallbackChange::OpenMenu {
1632 menu,
1633 position: None,
1634 });
1635 }
1636
1637 pub fn open_menu_at(&mut self, menu: Menu, position: LogicalPosition) {
1643 self.push_change(CallbackChange::OpenMenu {
1644 menu,
1645 position: Some(position),
1646 });
1647 }
1648
1649 pub fn show_tooltip(&mut self, text: AzString) {
1665 let position = self
1666 .get_cursor_relative_to_viewport()
1667 .into_option()
1668 .unwrap_or_else(LogicalPosition::zero);
1669 self.push_change(CallbackChange::ShowTooltip { text, position });
1670 }
1671
1672 pub fn show_tooltip_at(&mut self, text: AzString, position: LogicalPosition) {
1678 self.push_change(CallbackChange::ShowTooltip { text, position });
1679 }
1680
1681 pub fn hide_tooltip(&mut self) {
1683 self.push_change(CallbackChange::HideTooltip);
1684 }
1685
1686 pub fn insert_text(&mut self, dom_id: DomId, node_id: NodeId, text: AzString) {
1698 self.push_change(CallbackChange::InsertText {
1699 dom_id,
1700 node_id,
1701 text,
1702 });
1703 }
1704
1705 pub fn move_cursor(&mut self, dom_id: DomId, node_id: NodeId, cursor: TextCursor) {
1712 self.push_change(CallbackChange::MoveCursor {
1713 dom_id,
1714 node_id,
1715 cursor,
1716 });
1717 }
1718
1719 pub fn set_selection(&mut self, dom_id: DomId, node_id: NodeId, selection: Selection) {
1726 self.push_change(CallbackChange::SetSelection {
1727 dom_id,
1728 node_id,
1729 selection,
1730 });
1731 }
1732
1733 pub fn add_cursor(&mut self, dom_id: DomId, node_id: NodeId, cursor: TextCursor) -> azul_core::selection::SelectionId {
1742 let id = azul_core::selection::SelectionId::new();
1743 self.push_change(CallbackChange::AddCursor {
1744 dom_id,
1745 node_id,
1746 cursor,
1747 });
1748 id
1749 }
1750
1751 pub fn add_selection_range(&mut self, dom_id: DomId, node_id: NodeId, range: SelectionRange) -> azul_core::selection::SelectionId {
1755 let id = azul_core::selection::SelectionId::new();
1756 self.push_change(CallbackChange::AddSelectionRange {
1757 dom_id,
1758 node_id,
1759 range,
1760 });
1761 id
1762 }
1763
1764 pub fn remove_selection_by_id(&mut self, selection_id: azul_core::selection::SelectionId) -> bool {
1768 self.push_change(CallbackChange::RemoveSelectionById {
1769 selection_id,
1770 });
1771 true }
1773
1774 #[must_use] pub fn get_multi_cursor_selections(&self, dom_id: &DomId) -> azul_core::selection::IdentifiedSelectionVec {
1779 let lw = self.get_layout_window();
1780 lw.text_edit_manager.multi_cursor.as_ref()
1781 .map(|mc| mc.selections.clone())
1782 .unwrap_or_default()
1783 .into()
1784 }
1785
1786 #[must_use] pub fn get_primary_selection(&self, dom_id: &DomId) -> Option<azul_core::selection::IdentifiedSelection> {
1788 let lw = self.get_layout_window();
1789 lw.text_edit_manager.multi_cursor.as_ref()
1790 .and_then(|mc| mc.get_primary().copied())
1791 }
1792
1793 #[must_use] pub fn get_selection_count(&self, dom_id: &DomId) -> usize {
1795 let lw = self.get_layout_window();
1796 lw.text_edit_manager.multi_cursor.as_ref()
1797 .map_or(0, azul_core::selection::MultiCursorState::len)
1798 }
1799
1800 pub fn open_menu_for_node(&mut self, menu: Menu, node_id: DomNodeId) -> bool {
1813 let rect = self
1818 .get_node_hit_test_bounds(node_id)
1819 .or_else(|| self.get_node_rect(node_id));
1820 rect.is_some_and(|rect| {
1821 let position = LogicalPosition::new(rect.origin.x, rect.origin.y + rect.size.height);
1823 self.push_change(CallbackChange::OpenMenu {
1824 menu,
1825 position: Some(position),
1826 });
1827 true
1828 })
1829 }
1830
1831 pub fn open_menu_for_hit_node(&mut self, menu: Menu) -> bool {
1843 let hit_node = self.get_hit_node();
1844 self.open_menu_for_node(menu, hit_node)
1845 }
1846
1847 #[must_use] pub const fn get_layout_window(&self) -> &LayoutWindow {
1854 unsafe { (*self.ref_data).layout_window }
1855 }
1856
1857 fn get_inline_layout_for_node(&self, node_id: &DomNodeId) -> Option<&Arc<UnifiedLayout>> {
1868 let layout_window = self.get_layout_window();
1869
1870 let layout_result = layout_window.layout_results.get(&node_id.dom)?;
1872
1873 let dom_node_id = node_id.node.into_crate_internal()?;
1875
1876 let layout_indices = layout_result.layout_tree.dom_to_layout.get(&dom_node_id)?;
1878
1879 let layout_index = *layout_indices.first()?;
1882
1883 let warm_node = layout_result.layout_tree.warm(layout_index)?;
1885 warm_node
1886 .inline_layout_result
1887 .as_ref()
1888 .map(super::solver3::layout_tree::CachedInlineLayout::get_layout)
1889 }
1890
1891 #[must_use] pub fn get_node_size(&self, node_id: DomNodeId) -> Option<LogicalSize> {
1896 self.get_layout_window().get_node_size(node_id)
1897 }
1898
1899 #[must_use] pub fn get_node_position(&self, node_id: DomNodeId) -> Option<LogicalPosition> {
1901 self.get_layout_window().get_node_position(node_id)
1902 }
1903
1904 #[must_use] pub fn get_node_hit_test_bounds(&self, node_id: DomNodeId) -> Option<LogicalRect> {
1909 self.get_layout_window().get_node_hit_test_bounds(node_id)
1910 }
1911
1912 #[must_use] pub fn get_node_rect(&self, node_id: DomNodeId) -> Option<LogicalRect> {
1917 let position = self.get_node_position(node_id)?;
1918 let size = self.get_node_size(node_id)?;
1919 Some(LogicalRect::new(position, size))
1920 }
1921
1922 #[must_use] pub fn get_hit_node_rect(&self) -> Option<LogicalRect> {
1927 let hit_node = self.get_hit_node();
1928 self.get_node_rect(hit_node)
1929 }
1930
1931 #[must_use] pub fn get_timer(&self, timer_id: &TimerId) -> Option<&Timer> {
1935 self.get_layout_window().get_timer(timer_id)
1936 }
1937
1938 #[must_use] pub fn get_timer_ids(&self) -> TimerIdVec {
1940 self.get_layout_window().get_timer_ids()
1941 }
1942
1943 #[must_use] pub fn get_thread(&self, thread_id: &ThreadId) -> Option<&Thread> {
1947 self.get_layout_window().get_thread(thread_id)
1948 }
1949
1950 #[must_use] pub fn get_thread_ids(&self) -> ThreadIdVec {
1952 self.get_layout_window().get_thread_ids()
1953 }
1954
1955 #[must_use] pub fn get_gpu_cache(&self, dom_id: &DomId) -> Option<&GpuValueCache> {
1959 self.get_layout_window().get_gpu_cache(dom_id)
1960 }
1961
1962 #[must_use] pub fn get_layout_result(&self, dom_id: &DomId) -> Option<&DomLayoutResult> {
1966 self.get_layout_window().get_layout_result(dom_id)
1967 }
1968
1969 #[must_use] pub fn get_dom_ids(&self) -> DomIdVec {
1971 self.get_layout_window().get_dom_ids()
1972 }
1973
1974 #[must_use] pub const fn get_hit_node(&self) -> DomNodeId {
1978 self.hit_dom_node
1979 }
1980
1981 #[allow(clippy::trivially_copy_pass_by_ref)] fn is_node_anonymous(&self, dom_id: &DomId, node_id: NodeId) -> bool {
1984 let layout_window = self.get_layout_window();
1985 let Some(layout_result) = layout_window.get_layout_result(dom_id) else {
1986 return false;
1987 };
1988 let node_data_cont = layout_result.styled_dom.node_data.as_container();
1989 let Some(node_data) = node_data_cont.get(node_id) else {
1990 return false;
1991 };
1992 node_data.is_anonymous()
1993 }
1994
1995 #[must_use] pub fn get_parent(&self, node_id: DomNodeId) -> Option<DomNodeId> {
1997 let layout_window = self.get_layout_window();
1998 let layout_result = layout_window.get_layout_result(&node_id.dom)?;
1999 let node_id_internal = node_id.node.into_crate_internal()?;
2000 let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
2001 let hier_item = node_hierarchy.get(node_id_internal)?;
2002
2003 let mut current_parent_id = hier_item.parent_id()?;
2005 loop {
2006 if !self.is_node_anonymous(&node_id.dom, current_parent_id) {
2007 return Some(DomNodeId {
2008 dom: node_id.dom,
2009 node: NodeHierarchyItemId::from_crate_internal(Some(current_parent_id)),
2010 });
2011 }
2012
2013 let parent_hier_item = node_hierarchy.get(current_parent_id)?;
2015 current_parent_id = parent_hier_item.parent_id()?;
2016 }
2017 }
2018
2019 #[must_use] pub fn get_previous_sibling(&self, node_id: DomNodeId) -> Option<DomNodeId> {
2021 let layout_window = self.get_layout_window();
2022 let layout_result = layout_window.get_layout_result(&node_id.dom)?;
2023 let node_id_internal = node_id.node.into_crate_internal()?;
2024 let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
2025 let hier_item = node_hierarchy.get(node_id_internal)?;
2026
2027 let mut current_sibling_id = hier_item.previous_sibling_id()?;
2029 loop {
2030 if !self.is_node_anonymous(&node_id.dom, current_sibling_id) {
2031 return Some(DomNodeId {
2032 dom: node_id.dom,
2033 node: NodeHierarchyItemId::from_crate_internal(Some(current_sibling_id)),
2034 });
2035 }
2036
2037 let sibling_hier_item = node_hierarchy.get(current_sibling_id)?;
2039 current_sibling_id = sibling_hier_item.previous_sibling_id()?;
2040 }
2041 }
2042
2043 #[must_use] pub fn get_next_sibling(&self, node_id: DomNodeId) -> Option<DomNodeId> {
2045 let layout_window = self.get_layout_window();
2046 let layout_result = layout_window.get_layout_result(&node_id.dom)?;
2047 let node_id_internal = node_id.node.into_crate_internal()?;
2048 let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
2049 let hier_item = node_hierarchy.get(node_id_internal)?;
2050
2051 let mut current_sibling_id = hier_item.next_sibling_id()?;
2053 loop {
2054 if !self.is_node_anonymous(&node_id.dom, current_sibling_id) {
2055 return Some(DomNodeId {
2056 dom: node_id.dom,
2057 node: NodeHierarchyItemId::from_crate_internal(Some(current_sibling_id)),
2058 });
2059 }
2060
2061 let sibling_hier_item = node_hierarchy.get(current_sibling_id)?;
2063 current_sibling_id = sibling_hier_item.next_sibling_id()?;
2064 }
2065 }
2066
2067 #[must_use] pub fn get_first_child(&self, node_id: DomNodeId) -> Option<DomNodeId> {
2069 let layout_window = self.get_layout_window();
2070 let layout_result = layout_window.get_layout_result(&node_id.dom)?;
2071 let node_id_internal = node_id.node.into_crate_internal()?;
2072 let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
2073 let hier_item = node_hierarchy.get(node_id_internal)?;
2074
2075 let mut current_child_id = hier_item.first_child_id(node_id_internal)?;
2077 loop {
2078 if !self.is_node_anonymous(&node_id.dom, current_child_id) {
2079 return Some(DomNodeId {
2080 dom: node_id.dom,
2081 node: NodeHierarchyItemId::from_crate_internal(Some(current_child_id)),
2082 });
2083 }
2084
2085 let child_hier_item = node_hierarchy.get(current_child_id)?;
2087 current_child_id = child_hier_item.next_sibling_id()?;
2088 }
2089 }
2090
2091 #[must_use] pub fn get_last_child(&self, node_id: DomNodeId) -> Option<DomNodeId> {
2093 let layout_window = self.get_layout_window();
2094 let layout_result = layout_window.get_layout_result(&node_id.dom)?;
2095 let node_id_internal = node_id.node.into_crate_internal()?;
2096 let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
2097 let hier_item = node_hierarchy.get(node_id_internal)?;
2098
2099 let mut current_child_id = hier_item.last_child_id()?;
2101 loop {
2102 if !self.is_node_anonymous(&node_id.dom, current_child_id) {
2103 return Some(DomNodeId {
2104 dom: node_id.dom,
2105 node: NodeHierarchyItemId::from_crate_internal(Some(current_child_id)),
2106 });
2107 }
2108
2109 let child_hier_item = node_hierarchy.get(current_child_id)?;
2111 current_child_id = child_hier_item.previous_sibling_id()?;
2112 }
2113 }
2114
2115 pub fn get_dataset(&mut self, node_id: DomNodeId) -> Option<RefAny> {
2119 let layout_window = self.get_layout_window();
2120 let layout_result = layout_window.get_layout_result(&node_id.dom)?;
2121 let node_id_internal = node_id.node.into_crate_internal()?;
2122 let node_data_cont = layout_result.styled_dom.node_data.as_container();
2123 let node_data = node_data_cont.get(node_id_internal)?;
2124 node_data.get_dataset().cloned()
2125 }
2126
2127 #[allow(clippy::needless_pass_by_value)]
2130 pub fn get_node_id_of_root_dataset(&mut self, search_key: RefAny) -> Option<DomNodeId> {
2131 let mut found: Option<(u64, DomNodeId)> = None;
2132 let search_type_id = search_key.get_type_id();
2133
2134 for dom_id in self.get_dom_ids().as_ref().iter().copied() {
2135 let layout_window = self.get_layout_window();
2136 let Some(layout_result) = layout_window.get_layout_result(&dom_id) else {
2137 continue;
2138 };
2139
2140 let node_data_cont = layout_result.styled_dom.node_data.as_container();
2141 for (node_idx, node_data) in node_data_cont.iter().enumerate() {
2142 if let Some(dataset) = node_data.get_dataset().cloned() {
2143 if dataset.get_type_id() == search_type_id {
2144 let node_id = DomNodeId {
2145 dom: dom_id,
2146 node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(
2147 node_idx,
2148 ))),
2149 };
2150 let instance_id = dataset.instance_id;
2151
2152 match found {
2153 None => found = Some((instance_id, node_id)),
2154 Some((prev_instance, _)) => {
2155 if instance_id < prev_instance {
2156 found = Some((instance_id, node_id));
2157 }
2158 }
2159 }
2160 }
2161 }
2162 }
2163 }
2164
2165 found.map(|s| s.1)
2166 }
2167
2168 #[must_use] pub fn get_string_contents(&self, node_id: DomNodeId) -> Option<AzString> {
2170 let layout_window = self.get_layout_window();
2171 let layout_result = layout_window.get_layout_result(&node_id.dom)?;
2172 let node_id_internal = node_id.node.into_crate_internal()?;
2173 let node_data_cont = layout_result.styled_dom.node_data.as_container();
2174 let node_data = node_data_cont.get(node_id_internal)?;
2175
2176 if let NodeType::Text(text) = node_data.get_node_type() {
2177 Some(text.clone_self())
2178 } else {
2179 None
2180 }
2181 }
2182
2183 #[must_use] pub fn get_node_tag_name(&self, node_id: DomNodeId) -> Option<AzString> {
2188 let layout_window = self.get_layout_window();
2189 let layout_result = layout_window.get_layout_result(&node_id.dom)?;
2190 let node_id_internal = node_id.node.into_crate_internal()?;
2191 let node_data_cont = layout_result.styled_dom.node_data.as_container();
2192 let node_data = node_data_cont.get(node_id_internal)?;
2193
2194 let tag = node_data.get_node_type().get_path();
2195 Some(tag.to_string().into())
2196 }
2197
2198 #[allow(clippy::match_same_arms)]
2210 #[must_use] pub fn get_node_attribute(&self, node_id: DomNodeId, attr_name: &str) -> Option<AzString> {
2211 use azul_core::dom::AttributeType;
2212
2213 let layout_window = self.get_layout_window();
2214 let layout_result = layout_window.get_layout_result(&node_id.dom)?;
2215 let node_id_internal = node_id.node.into_crate_internal()?;
2216 let node_data_cont = layout_result.styled_dom.node_data.as_container();
2217 let node_data = node_data_cont.get(node_id_internal)?;
2218
2219 for attr in node_data.attributes().as_ref() {
2221 match (attr_name, attr) {
2222 ("id", AttributeType::Id(v)) => return Some(v.clone()),
2223 ("class", AttributeType::Class(v)) => return Some(v.clone()),
2224 ("aria-label", AttributeType::AriaLabel(v)) => return Some(v.clone()),
2225 ("aria-labelledby", AttributeType::AriaLabelledBy(v)) => return Some(v.clone()),
2226 ("aria-describedby", AttributeType::AriaDescribedBy(v)) => return Some(v.clone()),
2227 ("role", AttributeType::AriaRole(v)) => return Some(v.clone()),
2228 ("href", AttributeType::Href(v)) => return Some(v.clone()),
2229 ("rel", AttributeType::Rel(v)) => return Some(v.clone()),
2230 ("target", AttributeType::Target(v)) => return Some(v.clone()),
2231 ("src", AttributeType::Src(v)) => return Some(v.clone()),
2232 ("alt", AttributeType::Alt(v)) => return Some(v.clone()),
2233 ("title", AttributeType::Title(v)) => return Some(v.clone()),
2234 ("name", AttributeType::Name(v)) => return Some(v.clone()),
2235 ("value", AttributeType::Value(v)) => return Some(v.clone()),
2236 ("type", AttributeType::InputType(v)) => return Some(v.clone()),
2237 ("placeholder", AttributeType::Placeholder(v)) => return Some(v.clone()),
2238 ("max", AttributeType::Max(v)) => return Some(v.clone()),
2239 ("min", AttributeType::Min(v)) => return Some(v.clone()),
2240 ("step", AttributeType::Step(v)) => return Some(v.clone()),
2241 ("pattern", AttributeType::Pattern(v)) => return Some(v.clone()),
2242 ("autocomplete", AttributeType::Autocomplete(v)) => return Some(v.clone()),
2243 ("scope", AttributeType::Scope(v)) => return Some(v.clone()),
2244 ("lang", AttributeType::Lang(v)) => return Some(v.clone()),
2245 ("dir", AttributeType::Dir(v)) => return Some(v.clone()),
2246 ("required", AttributeType::Required) => return Some("true".into()),
2247 ("disabled", AttributeType::Disabled) => return Some("true".into()),
2248 ("readonly", AttributeType::Readonly) => return Some("true".into()),
2249 ("checked", AttributeType::CheckedTrue) => return Some("true".into()),
2250 ("checked", AttributeType::CheckedFalse) => return Some("false".into()),
2251 ("selected", AttributeType::Selected) => return Some("true".into()),
2252 ("hidden", AttributeType::Hidden) => return Some("true".into()),
2253 ("focusable", AttributeType::Focusable) => return Some("true".into()),
2254 ("minlength", AttributeType::MinLength(v)) => return Some(v.to_string().into()),
2255 ("maxlength", AttributeType::MaxLength(v)) => return Some(v.to_string().into()),
2256 ("colspan", AttributeType::ColSpan(v)) => return Some(v.to_string().into()),
2257 ("rowspan", AttributeType::RowSpan(v)) => return Some(v.to_string().into()),
2258 ("tabindex", AttributeType::TabIndex(v)) => return Some(v.to_string().into()),
2259 ("contenteditable", AttributeType::ContentEditable(v)) => {
2260 return Some(v.to_string().into())
2261 }
2262 ("draggable", AttributeType::Draggable(v)) => return Some(v.to_string().into()),
2263 (name, AttributeType::Data(nv))
2265 if name.starts_with("data-") && nv.attr_name.as_str() == &name[5..] =>
2266 {
2267 return Some(nv.value.clone());
2268 }
2269 (name, AttributeType::AriaState(nv))
2271 if name == format!("aria-{}", nv.attr_name.as_str()) =>
2272 {
2273 return Some(nv.value.clone());
2274 }
2275 (name, AttributeType::AriaProperty(nv))
2276 if name == format!("aria-{}", nv.attr_name.as_str()) =>
2277 {
2278 return Some(nv.value.clone());
2279 }
2280 (name, AttributeType::Custom(nv)) if nv.attr_name.as_str() == name => {
2282 return Some(nv.value.clone());
2283 }
2284 _ => {}
2285 }
2286 }
2287
2288 None
2289 }
2290
2291 #[must_use] pub fn get_node_classes(&self, node_id: DomNodeId) -> StringVec {
2293 let Some(layout_window) = self.get_layout_window().get_layout_result(&node_id.dom) else {
2294 return StringVec::from_const_slice(&[]);
2295 };
2296 let Some(node_id_internal) = node_id.node.into_crate_internal() else {
2297 return StringVec::from_const_slice(&[]);
2298 };
2299 let node_data_cont = layout_window.styled_dom.node_data.as_container();
2300 let Some(node_data) = node_data_cont.get(node_id_internal) else {
2301 return StringVec::from_const_slice(&[]);
2302 };
2303
2304 let classes: Vec<AzString> = node_data
2305 .attributes()
2306 .as_ref()
2307 .iter()
2308 .filter_map(|attr| {
2309 attr.as_class().map(|c| c.to_string().into())
2310 })
2311 .collect();
2312
2313 StringVec::from(classes)
2314 }
2315
2316 #[must_use] pub fn get_node_id(&self, node_id: DomNodeId) -> Option<AzString> {
2318 let layout_window = self.get_layout_window();
2319 let layout_result = layout_window.get_layout_result(&node_id.dom)?;
2320 let node_id_internal = node_id.node.into_crate_internal()?;
2321 let node_data_cont = layout_result.styled_dom.node_data.as_container();
2322 let node_data = node_data_cont.get(node_id_internal)?;
2323
2324 for attr in node_data.attributes().as_ref() {
2325 if let Some(id) = attr.as_id() {
2326 return Some(id.to_string().into());
2327 }
2328 }
2329 None
2330 }
2331
2332 #[must_use] pub const fn get_selection(&self, _dom_id: &DomId) -> Option<&SelectionState> {
2336 None
2339 }
2340
2341 #[must_use] pub fn has_selection(&self, _dom_id: &DomId) -> bool {
2343 self.get_layout_window()
2344 .text_edit_manager.multi_cursor.as_ref()
2345 .is_some_and(|mc| mc.selections.iter().any(|s| matches!(&s.selection, Selection::Range(_))))
2346 }
2347
2348 #[must_use] pub fn get_primary_cursor(&self, _dom_id: &DomId) -> Option<TextCursor> {
2350 self.get_layout_window()
2351 .text_edit_manager.multi_cursor.as_ref()
2352 .and_then(azul_core::selection::MultiCursorState::get_primary_cursor)
2353 }
2354
2355 #[must_use] pub fn get_selection_ranges(&self, _dom_id: &DomId) -> SelectionRangeVec {
2357 let ranges: Vec<SelectionRange> = self.get_layout_window()
2358 .text_edit_manager.multi_cursor.as_ref()
2359 .map(|mc| mc.selections.iter().filter_map(|s| match &s.selection {
2360 Selection::Range(r) => Some(*r),
2361 Selection::Cursor(_) => None,
2362 }).collect()).unwrap_or_default();
2363 ranges.into()
2364 }
2365
2366 #[must_use] pub const fn get_text_cache(&self) -> &TextLayoutCache {
2379 &self.get_layout_window().text_cache
2380 }
2381
2382 #[must_use] pub const fn get_current_window_state(&self) -> &FullWindowState {
2386 unsafe { (*self.ref_data).current_window_state }
2388 }
2389
2390 #[must_use] pub const fn get_current_window_flags(&self) -> WindowFlags {
2392 self.get_current_window_state().flags
2393 }
2394
2395 #[must_use] pub fn get_current_keyboard_state(&self) -> KeyboardState {
2397 self.get_current_window_state().keyboard_state.clone()
2398 }
2399
2400 #[must_use] pub const fn get_current_mouse_state(&self) -> MouseState {
2402 self.get_current_window_state().mouse_state
2403 }
2404
2405 #[must_use] pub const fn get_previous_window_state(&self) -> &Option<FullWindowState> {
2407 unsafe { (*self.ref_data).previous_window_state }
2408 }
2409
2410 #[must_use] pub fn get_previous_window_flags(&self) -> Option<WindowFlags> {
2412 Some(self.get_previous_window_state().as_ref()?.flags)
2413 }
2414
2415 #[must_use] pub fn get_previous_keyboard_state(&self) -> Option<KeyboardState> {
2417 Some(
2418 self.get_previous_window_state()
2419 .as_ref()?
2420 .keyboard_state
2421 .clone(),
2422 )
2423 }
2424
2425 #[must_use] pub fn get_previous_mouse_state(&self) -> Option<MouseState> {
2427 Some(
2428 self.get_previous_window_state()
2429 .as_ref()?
2430 .mouse_state,
2431 )
2432 }
2433
2434 #[must_use] pub const fn get_cursor_relative_to_node(&self) -> azul_core::geom::OptionCursorNodePosition {
2437 use azul_core::geom::{CursorNodePosition, OptionCursorNodePosition};
2438 match self.cursor_relative_to_item {
2439 OptionLogicalPosition::Some(p) => OptionCursorNodePosition::Some(CursorNodePosition::from_logical(p)),
2440 OptionLogicalPosition::None => OptionCursorNodePosition::None,
2441 }
2442 }
2443
2444 #[must_use] pub const fn get_cursor_relative_to_viewport(&self) -> OptionLogicalPosition {
2445 self.cursor_in_viewport
2446 }
2447
2448 #[allow(clippy::cast_precision_loss)] #[must_use] pub fn get_cursor_position_screen(&self) -> azul_core::geom::OptionScreenPosition {
2471 use azul_core::window::WindowPosition;
2472 use azul_core::geom::{LogicalPosition, ScreenPosition, OptionScreenPosition};
2473
2474 let ws = self.get_current_window_state();
2475 let Some(cursor_local) = ws.mouse_state.cursor_position.get_position() else {
2476 return OptionScreenPosition::None;
2477 };
2478 match ws.position {
2479 WindowPosition::Initialized(pos) => {
2480 OptionScreenPosition::Some(ScreenPosition::new(
2481 pos.x as f32 + cursor_local.x,
2482 pos.y as f32 + cursor_local.y,
2483 ))
2484 }
2485 WindowPosition::Uninitialized | WindowPosition::RelativeToParentWindow(_) => {
2488 OptionScreenPosition::Some(ScreenPosition::new(cursor_local.x, cursor_local.y))
2489 }
2490 }
2491 }
2492
2493 #[must_use] pub fn get_drag_delta(&self) -> azul_core::geom::OptionDragDelta {
2501 use azul_core::geom::{DragDelta, OptionDragDelta};
2502 let gm = self.get_gesture_drag_manager();
2503 match gm.get_drag_delta() {
2504 Some((dx, dy)) => OptionDragDelta::Some(DragDelta::new(dx, dy)),
2505 None => OptionDragDelta::None,
2506 }
2507 }
2508
2509 #[must_use] pub fn get_drag_delta_screen(&self) -> azul_core::geom::OptionDragDelta {
2515 use azul_core::geom::{DragDelta, OptionDragDelta};
2516 let gm = self.get_gesture_drag_manager();
2517 match gm.get_drag_delta_screen() {
2518 Some((dx, dy)) => OptionDragDelta::Some(DragDelta::new(dx, dy)),
2519 None => OptionDragDelta::None,
2520 }
2521 }
2522
2523 #[must_use] pub fn get_drag_delta_screen_incremental(&self) -> azul_core::geom::OptionDragDelta {
2537 use azul_core::geom::{DragDelta, OptionDragDelta};
2538 let gm = self.get_gesture_drag_manager();
2539 match gm.get_drag_delta_screen_incremental() {
2540 Some((dx, dy)) => OptionDragDelta::Some(DragDelta::new(dx, dy)),
2541 None => OptionDragDelta::None,
2542 }
2543 }
2544
2545 #[must_use] pub const fn get_current_window_handle(&self) -> RawWindowHandle {
2546 unsafe { *(*self.ref_data).current_window_handle }
2547 }
2548
2549 #[must_use] pub fn get_system_style(&self) -> Arc<SystemStyle> {
2552 unsafe { (*self.ref_data).system_style.clone() }
2553 }
2554
2555 #[must_use] pub fn get_monitors(&self) -> MonitorVec {
2561 let monitors_arc = unsafe { &(*self.ref_data).monitors };
2562 monitors_arc.lock().map_or_else(|_| MonitorVec::from_const_slice(&[]), |g| g.clone())
2563 }
2564
2565 #[must_use] pub fn get_current_monitor(&self) -> OptionMonitor {
2571 let ws = self.get_current_window_state();
2572 let monitor_index = match ws.monitor_id {
2573 azul_css::corety::OptionU32::Some(idx) => idx as usize,
2574 azul_css::corety::OptionU32::None => return OptionMonitor::None,
2575 };
2576 let monitors_arc = unsafe { &(*self.ref_data).monitors };
2577 let Ok(guard) = monitors_arc.lock() else {
2578 return OptionMonitor::None;
2579 };
2580 for m in guard.as_ref() {
2581 if m.monitor_id.index == monitor_index {
2582 return OptionMonitor::Some(m.clone());
2583 }
2584 }
2585 OptionMonitor::None
2586 }
2587
2588 #[cfg(feature = "icu")]
2603 pub fn get_icu_localizer(&self) -> &IcuLocalizerHandle {
2604 unsafe { &(*self.ref_data).icu_localizer }
2605 }
2606
2607 #[cfg(feature = "icu")]
2620 pub fn format_integer(&self, locale: &str, value: i64) -> AzString {
2621 self.get_icu_localizer().format_integer(locale, value)
2622 }
2623
2624 #[cfg(feature = "icu")]
2637 pub fn format_decimal(&self, locale: &str, integer_part: i64, decimal_places: i16) -> AzString {
2638 self.get_icu_localizer().format_decimal(locale, integer_part, decimal_places)
2639 }
2640
2641 #[cfg(feature = "icu")]
2655 pub fn get_plural_category(&self, locale: &str, value: i64) -> PluralCategory {
2656 self.get_icu_localizer().get_plural_category(locale, value)
2657 }
2658
2659 #[cfg(feature = "icu")]
2672 pub fn pluralize(
2673 &self,
2674 locale: &str,
2675 value: i64,
2676 zero: &str,
2677 one: &str,
2678 two: &str,
2679 few: &str,
2680 many: &str,
2681 other: &str,
2682 ) -> AzString {
2683 self.get_icu_localizer().pluralize(locale, value, zero, one, two, few, many, other)
2684 }
2685
2686 #[cfg(feature = "icu")]
2699 pub fn format_list(&self, locale: &str, items: StringVec, list_type: ListType) -> AzString {
2700 self.get_icu_localizer()
2701 .format_list(locale, items.as_ref(), list_type)
2702 }
2703
2704 #[cfg(feature = "icu")]
2718 pub fn format_date(&self, locale: &str, date: IcuDate, length: FormatLength) -> IcuResult {
2719 self.get_icu_localizer().format_date(locale, date, length)
2720 }
2721
2722 #[cfg(feature = "icu")]
2736 pub fn format_time(&self, locale: &str, time: IcuTime, include_seconds: bool) -> IcuResult {
2737 self.get_icu_localizer().format_time(locale, time, include_seconds)
2738 }
2739
2740 #[cfg(feature = "icu")]
2747 pub fn format_datetime(&self, locale: &str, datetime: IcuDateTime, length: FormatLength) -> IcuResult {
2748 self.get_icu_localizer().format_datetime(locale, datetime, length)
2749 }
2750
2751 #[cfg(feature = "icu")]
2767 pub fn compare_strings(&self, locale: &str, a: &str, b: &str) -> i32 {
2768 self.get_icu_localizer().compare_strings(locale, a, b)
2769 }
2770
2771 #[cfg(feature = "icu")]
2786 pub fn sort_strings(&self, locale: &str, strings: StringVec) -> IcuStringVec {
2787 self.get_icu_localizer()
2788 .sort_strings(locale, strings.as_ref())
2789 }
2790
2791 #[cfg(feature = "icu")]
2801 pub fn strings_equal(&self, locale: &str, a: &str, b: &str) -> bool {
2802 self.get_icu_localizer().strings_equal(locale, a, b)
2803 }
2804
2805 #[must_use] pub fn get_cursor_position(&self) -> Option<LogicalPosition> {
2807 self.cursor_in_viewport.into_option()
2808 }
2809
2810 #[must_use] pub fn get_hit_node_layout_rect(&self) -> Option<LogicalRect> {
2812 self.get_layout_window()
2813 .get_node_layout_rect(self.hit_dom_node)
2814 }
2815
2816 #[must_use] pub fn get_computed_css_property(
2836 &self,
2837 node_id: DomNodeId,
2838 property_type: CssPropertyType,
2839 ) -> Option<CssProperty> {
2840 let layout_window = self.get_layout_window();
2841
2842 let layout_result = layout_window.layout_results.get(&node_id.dom)?;
2844
2845 let styled_dom = &layout_result.styled_dom;
2847
2848 let internal_node_id = node_id.node.into_crate_internal()?;
2850
2851 let node_data_container = styled_dom.node_data.as_container();
2853 let node_data = node_data_container.get(internal_node_id)?;
2854
2855 let styled_nodes_container = styled_dom.styled_nodes.as_container();
2857 let styled_node = styled_nodes_container.get(internal_node_id)?;
2858 let node_state = &styled_node.styled_node_state;
2859
2860 let css_property_cache = &styled_dom.css_property_cache.ptr;
2862 css_property_cache
2863 .get_property(node_data, &internal_node_id, node_state, &property_type)
2864 .cloned()
2865 }
2866
2867 #[must_use] pub fn get_computed_width(&self, node_id: DomNodeId) -> Option<CssProperty> {
2871 self.get_computed_css_property(node_id, CssPropertyType::Width)
2872 }
2873
2874 #[must_use] pub fn get_computed_height(&self, node_id: DomNodeId) -> Option<CssProperty> {
2878 self.get_computed_css_property(node_id, CssPropertyType::Height)
2879 }
2880
2881 #[must_use] pub const fn get_system_time_fn(&self) -> GetSystemTimeCallback {
2884 unsafe { (*self.ref_data).system_callbacks.get_system_time_fn }
2885 }
2886
2887 #[must_use] pub fn get_current_time(&self) -> task::Instant {
2888 let cb = self.get_system_time_fn();
2889 (cb.cb)()
2890 }
2891
2892 #[must_use] pub const fn get_renderer_resources(&self) -> &RendererResources {
2897 unsafe { (*self.ref_data).renderer_resources }
2898 }
2899
2900 #[cfg(feature = "text_layout")]
2934 #[must_use] pub fn get_loaded_fonts(&self) -> LoadedFontVec {
2935 let font_manager = &self.get_layout_window().font_manager;
2936 let Ok(guard) = font_manager.parsed_fonts.lock() else {
2937 return Vec::new().into();
2938 };
2939 let mut out: Vec<LoadedFont> = guard
2942 .values()
2943 .map(|font_ref| {
2944 let parsed = crate::font_ref_to_parsed_font(font_ref);
2945 let family_name = parsed
2946 .font_name
2947 .as_ref()
2948 .map(|s| AzString::from(s.clone()))
2949 .unwrap_or_default();
2950 LoadedFont {
2951 font_hash: parsed.hash,
2952 family_name,
2953 num_glyphs: u32::from(parsed.num_glyphs),
2954 has_bytes: parsed.source_bytes_for_subset().is_some(),
2955 }
2956 })
2957 .collect();
2958 out.sort_by(|a, b| a.font_hash.cmp(&b.font_hash));
2959 out.into()
2960 }
2961
2962 #[cfg(feature = "text_layout")]
2972 #[must_use] pub fn get_loaded_font_bytes(&self, font_hash: u64) -> OptionU8Vec {
2973 let font_manager = &self.get_layout_window().font_manager;
2974 let Some(font_ref) = font_manager.get_font_by_hash(font_hash) else {
2975 return OptionU8Vec::None;
2976 };
2977 let parsed = crate::font_ref_to_parsed_font(&font_ref);
2978 parsed.source_bytes_for_subset().map_or_else(|| OptionU8Vec::None, |bytes| OptionU8Vec::Some(U8Vec::from_vec(bytes.as_slice().to_vec())))
2979 }
2980
2981 #[cfg(feature = "cpurender")]
3010 pub fn take_screenshot(&self, dom_id: DomId) -> Result<alloc::vec::Vec<u8>, AzString> {
3014 use crate::cpurender::{render_with_font_manager_and_scroll, CpuRenderState, RenderOptions, ScrollOffsetMap};
3015
3016 let layout_window = self.get_layout_window();
3017 let renderer_resources = &layout_window.renderer_resources;
3018
3019 let layout_result = layout_window
3021 .layout_results
3022 .get(&dom_id)
3023 .ok_or_else(|| AzString::from("DOM not found in layout results"))?;
3024
3025 let ws = self.get_current_window_state();
3027 let width = ws.size.dimensions.width;
3028 let height = ws.size.dimensions.height;
3029
3030 if width <= 0.0 || height <= 0.0 {
3031 return Err(AzString::from("Invalid viewport dimensions"));
3032 }
3033
3034 let display_list = &layout_result.display_list;
3035 let dpi_factor = ws.size.get_hidpi_factor().inner.get();
3036
3037 let scroll_offsets = layout_window.scroll_manager
3039 .build_scroll_offset_map(dom_id, &layout_result.scroll_ids);
3040
3041 let gpu_cache = layout_window.gpu_state_manager
3045 .get_cache(dom_id);
3046 let render_state = CpuRenderState::from_gpu_cache(
3047 gpu_cache,
3048 dom_id,
3049 &scroll_offsets,
3050 )
3051 .with_system_style(layout_window.system_style.clone());
3052
3053 let opts = RenderOptions {
3054 width,
3055 height,
3056 dpi_factor,
3057 };
3058
3059 let mut glyph_cache = crate::glyph_cache::GlyphCache::new();
3060 let pixmap = render_with_font_manager_and_scroll(
3061 display_list,
3062 renderer_resources,
3063 &layout_window.font_manager,
3064 opts,
3065 &mut glyph_cache,
3066 &render_state,
3067 ).map_err(AzString::from)?;
3068
3069 let png_data = pixmap
3071 .encode_png()
3072 .map_err(|e| AzString::from(alloc::format!("PNG encoding failed: {e}")))?;
3073
3074 Ok(png_data)
3075 }
3076
3077 #[cfg(all(feature = "std", feature = "cpurender"))]
3089 pub fn take_screenshot_to_file(&self, dom_id: DomId, path: &str) -> Result<(), AzString> {
3093 let png_data = self.take_screenshot(dom_id)?;
3094 std::fs::write(path, png_data)
3095 .map_err(|e| AzString::from(alloc::format!("Failed to write file: {e}")))?;
3096 Ok(())
3097 }
3098
3099 #[cfg(feature = "std")]
3108 pub fn take_native_screenshot(&self, _path: &str) -> Result<(), AzString> {
3112 Err(AzString::from(
3113 "Native screenshot requires the NativeScreenshotExt trait from azul-dll crate. \
3114 Import it with: use azul::desktop::NativeScreenshotExt;",
3115 ))
3116 }
3117
3118 #[cfg(feature = "std")]
3127 pub fn take_native_screenshot_bytes(&self) -> Result<alloc::vec::Vec<u8>, AzString> {
3131 let temp_path = std::env::temp_dir().join("azul_screenshot_temp.png");
3133 let temp_path_str = temp_path.to_string_lossy().to_string();
3134
3135 self.take_native_screenshot(&temp_path_str)?;
3136
3137 let bytes = std::fs::read(&temp_path)
3138 .map_err(|e| AzString::from(alloc::format!("Failed to read screenshot: {e}")))?;
3139
3140 drop(std::fs::remove_file(&temp_path));
3141
3142 Ok(bytes)
3143 }
3144
3145 #[cfg(feature = "std")]
3155 pub fn take_native_screenshot_base64(&self) -> Result<AzString, AzString> {
3159 let png_bytes = self.take_native_screenshot_bytes()?;
3160 let base64_str = base64_encode(&png_bytes);
3161 Ok(AzString::from(alloc::format!(
3162 "data:image/png;base64,{base64_str}"
3163 )))
3164 }
3165
3166 #[cfg(feature = "cpurender")]
3175 pub fn take_screenshot_base64(&self, dom_id: DomId) -> Result<AzString, AzString> {
3179 let png_bytes = self.take_screenshot(dom_id)?;
3180 let base64_str = base64_encode(&png_bytes);
3181 Ok(AzString::from(alloc::format!(
3182 "data:image/png;base64,{base64_str}"
3183 )))
3184 }
3185
3186 #[must_use] pub const fn get_scroll_manager(&self) -> &ScrollManager {
3193 unsafe { &(*self.ref_data).layout_window.scroll_manager }
3194 }
3195
3196 #[must_use] pub const fn get_gesture_drag_manager(&self) -> &GestureAndDragManager {
3204 unsafe { &(*self.ref_data).layout_window.gesture_drag_manager }
3205 }
3206
3207 pub fn inject_native_gesture(
3215 &mut self,
3216 gesture: crate::managers::gesture::NativeGestureEvent,
3217 ) {
3218 self.push_change(CallbackChange::InjectNativeGesture { gesture });
3219 }
3220
3221 #[must_use] pub const fn get_focus_manager(&self) -> &FocusManager {
3226 &self.get_layout_window().focus_manager
3227 }
3228
3229 #[must_use] pub const fn get_undo_redo_manager(&self) -> &UndoRedoManager {
3234 &self.get_layout_window().undo_redo_manager
3235 }
3236
3237 #[must_use] pub const fn get_hover_manager(&self) -> &HoverManager {
3242 &self.get_layout_window().hover_manager
3243 }
3244
3245 #[must_use] pub const fn get_text_input_manager(&self) -> &TextInputManager {
3249 &self.get_layout_window().text_input_manager
3250 }
3251
3252 #[must_use] pub fn has_any_selection(&self) -> bool {
3256 self.get_layout_window()
3257 .text_edit_manager.multi_cursor.as_ref()
3258 .is_some_and(|mc| mc.selections.iter().any(|s| matches!(&s.selection, Selection::Range(_))))
3259 }
3260
3261 #[must_use] pub fn is_node_focused(&self, node_id: DomNodeId) -> bool {
3263 self.get_focus_manager().has_focus(&node_id)
3264 }
3265
3266 #[must_use] pub fn is_dom_focused(&self, dom_id: DomId) -> bool {
3268 self.get_focused_node()
3269 .is_some_and(|n| n.dom == dom_id)
3270 }
3271
3272 #[must_use] pub const fn get_pen_state(&self) -> Option<&PenState> {
3276 self.get_gesture_drag_manager().get_pen_state()
3277 }
3278
3279 #[must_use] pub const fn get_wacom_pad(&self) -> Option<crate::managers::gesture::WacomPadState> {
3285 self.get_gesture_drag_manager().get_pad_state().copied()
3286 }
3287
3288 #[must_use] pub const fn get_location_fix(&self) -> Option<azul_core::geolocation::LocationFix> {
3295 self.get_layout_window().geolocation_manager.latest_fix()
3296 }
3297
3298 #[must_use] pub const fn get_sensor_reading(
3304 &self,
3305 kind: azul_core::sensors::SensorKind,
3306 ) -> Option<azul_core::sensors::SensorReading> {
3307 self.get_layout_window().sensor_manager.reading(kind)
3308 }
3309
3310 #[must_use] pub const fn get_safe_area_insets(&self) -> azul_css::system::SafeAreaInsets {
3316 self.get_layout_window().safe_area_insets
3317 }
3318
3319 #[must_use] pub fn get_gamepad_state(
3327 &self,
3328 id: azul_core::gamepad::GamepadId,
3329 ) -> Option<azul_core::gamepad::GamepadState> {
3330 self.get_layout_window().gamepad_manager.state(id)
3331 }
3332
3333 #[must_use] pub fn get_primary_gamepad(&self) -> Option<azul_core::gamepad::GamepadState> {
3336 self.get_layout_window().gamepad_manager.primary()
3337 }
3338
3339 #[must_use] pub const fn get_biometric_result(&self) -> Option<azul_core::biometric::BiometricResult> {
3346 self.get_layout_window().biometric_manager.last_result()
3347 }
3348
3349 #[must_use] pub const fn get_biometric_kind(&self) -> azul_core::biometric::BiometricKind {
3354 self.get_layout_window().biometric_manager.availability()
3355 }
3356
3357 pub fn request_biometric_auth(&mut self, prompt: azul_core::biometric::BiometricPrompt) {
3368 crate::managers::biometric::push_biometric_request(prompt);
3369 }
3370
3371 pub fn keyring_store(&mut self, key: AzString, secret: AzString, require_biometry: bool) {
3377 crate::managers::keyring::push_keyring_request(
3378 azul_core::keyring::KeyringRequest::Store {
3379 key,
3380 secret,
3381 require_biometry,
3382 },
3383 );
3384 }
3385
3386 pub fn keyring_get(&mut self, key: AzString) {
3390 crate::managers::keyring::push_keyring_request(azul_core::keyring::KeyringRequest::Get {
3391 key,
3392 });
3393 }
3394
3395 pub fn keyring_delete(&mut self, key: AzString) {
3398 crate::managers::keyring::push_keyring_request(
3399 azul_core::keyring::KeyringRequest::Delete { key },
3400 );
3401 }
3402
3403 #[must_use] pub fn get_keyring_result(&self) -> Option<azul_core::keyring::KeyringResult> {
3408 self.get_layout_window().keyring_manager.last_result().cloned()
3409 }
3410
3411 #[must_use] pub fn get_permission_status(
3418 &self,
3419 capability: crate::managers::permission::Capability,
3420 ) -> crate::managers::permission::PermissionState {
3421 self.get_layout_window()
3422 .permission_manager
3423 .get_status(capability)
3424 }
3425
3426 #[must_use] pub fn get_pen_pressure(&self) -> Option<f32> {
3429 self.get_pen_state().map(|pen| pen.pressure)
3430 }
3431
3432 #[must_use] pub fn get_pen_tilt(&self) -> Option<PenTilt> {
3435 self.get_pen_state().map(|pen| pen.tilt)
3436 }
3437
3438 #[must_use] pub fn is_pen_in_contact(&self) -> bool {
3440 self.get_pen_state()
3441 .is_some_and(|pen| pen.in_contact)
3442 }
3443
3444 #[must_use] pub fn is_pen_eraser(&self) -> bool {
3446 self.get_pen_state()
3447 .is_some_and(|pen| pen.is_eraser)
3448 }
3449
3450 #[must_use] pub fn is_pen_barrel_button_pressed(&self) -> bool {
3452 self.get_pen_state()
3453 .is_some_and(|pen| pen.barrel_button_pressed)
3454 }
3455
3456 #[must_use] pub fn get_last_input_sample(&self) -> Option<&InputSample> {
3458 let manager = self.get_gesture_drag_manager();
3459 manager
3460 .get_current_session()
3461 .and_then(|session| session.last_sample())
3462 }
3463
3464 #[must_use] pub fn get_current_event_id(&self) -> Option<u64> {
3466 self.get_last_input_sample().map(|sample| sample.event_id)
3467 }
3468
3469 #[must_use] pub fn get_swipe_direction(&self) -> crate::managers::gesture::OptionGestureDirection {
3484 self.get_gesture_drag_manager().detect_swipe_direction().into()
3485 }
3486
3487 #[must_use] pub fn get_pinch(&self) -> crate::managers::gesture::OptionDetectedPinch {
3489 self.get_gesture_drag_manager().detect_pinch().into()
3490 }
3491
3492 #[must_use] pub fn get_rotation(&self) -> crate::managers::gesture::OptionDetectedRotation {
3494 self.get_gesture_drag_manager().detect_rotation().into()
3495 }
3496
3497 #[must_use] pub fn get_long_press(&self) -> crate::managers::gesture::OptionDetectedLongPress {
3500 self.get_gesture_drag_manager().detect_long_press().into()
3501 }
3502
3503 #[must_use] pub fn was_double_clicked(&self) -> bool {
3506 self.get_gesture_drag_manager().detect_double_click()
3507 }
3508
3509 pub fn set_focus_to_node(&mut self, dom_id: DomId, node_id: NodeId) {
3513 self.set_focus(FocusTarget::Id(DomNodeId {
3514 dom: dom_id,
3515 node: NodeHierarchyItemId::from_crate_internal(Some(node_id)),
3516 }));
3517 }
3518
3519 pub fn set_focus_to_path(&mut self, dom_id: DomId, css_path: CssPath) {
3521 self.set_focus(FocusTarget::Path(FocusTargetPath {
3522 dom: dom_id,
3523 css_path,
3524 }));
3525 }
3526
3527 pub fn focus_next(&mut self) {
3529 self.set_focus(FocusTarget::Next);
3530 }
3531
3532 pub fn focus_previous(&mut self) {
3534 self.set_focus(FocusTarget::Previous);
3535 }
3536
3537 pub fn focus_first(&mut self) {
3539 self.set_focus(FocusTarget::First);
3540 }
3541
3542 pub fn focus_last(&mut self) {
3544 self.set_focus(FocusTarget::Last);
3545 }
3546
3547 pub fn clear_focus(&mut self) {
3549 self.set_focus(FocusTarget::NoFocus);
3550 }
3551
3552 #[must_use] pub const fn is_dragging(&self) -> bool {
3558 self.get_gesture_drag_manager().is_dragging()
3559 }
3560
3561 #[must_use] pub const fn get_focused_node(&self) -> Option<DomNodeId> {
3565 self.get_layout_window()
3566 .focus_manager
3567 .get_focused_node()
3568 .copied()
3569 }
3570
3571 #[must_use] pub fn has_focus(&self, node_id: DomNodeId) -> bool {
3573 self.get_layout_window().focus_manager.has_focus(&node_id)
3574 }
3575
3576 #[must_use] pub fn get_hovered_file(&self) -> Option<&AzString> {
3583 self.get_layout_window()
3584 .file_drop_manager
3585 .get_hovered_file()
3586 }
3587
3588 #[must_use] pub fn get_hovered_files(&self) -> StringVec {
3591 self.get_layout_window()
3592 .file_drop_manager
3593 .get_hovered_files()
3594 .to_vec()
3595 .into()
3596 }
3597
3598 #[must_use] pub fn get_dropped_file(&self) -> Option<&AzString> {
3604 self.get_layout_window()
3605 .file_drop_manager
3606 .get_dropped_file()
3607 }
3608
3609 #[must_use] pub fn get_dropped_files(&self) -> StringVec {
3611 self.get_layout_window()
3612 .file_drop_manager
3613 .get_dropped_files()
3614 .to_vec()
3615 .into()
3616 }
3617
3618 #[cfg(feature = "std")]
3626 #[must_use] pub fn measure_dom(
3627 &self,
3628 dom: azul_core::dom::Dom,
3629 available: LogicalSize,
3630 ) -> LogicalSize {
3631 self.get_layout_window().measure_dom(dom, available)
3632 }
3633
3634 #[must_use] pub fn get_deepest_hovered_node(&self) -> Option<DomNodeId> {
3639 let hit = self
3640 .get_layout_window()
3641 .hover_manager
3642 .get_current(&InputPointId::Mouse)?;
3643 hit.hovered_nodes.iter().next().and_then(|(dom_id, entry)| {
3644 entry.regular_hit_test_nodes.keys().next_back().map(|nid| DomNodeId {
3645 dom: *dom_id,
3646 node: NodeHierarchyItemId::from_crate_internal(Some(*nid)),
3647 })
3648 })
3649 }
3650
3651 #[must_use] pub const fn is_drag_active(&self) -> bool {
3657 self.get_layout_window().gesture_drag_manager.is_dragging()
3658 }
3659
3660 #[must_use] pub fn is_node_drag_active(&self) -> bool {
3662 self.get_layout_window().gesture_drag_manager.is_node_drag_active()
3663 }
3664
3665 #[must_use] pub fn is_file_drag_active(&self) -> bool {
3667 let lw = self.get_layout_window();
3668 lw.gesture_drag_manager.is_file_dropping()
3673 || !lw.file_drop_manager.get_hovered_files().is_empty()
3674 }
3675
3676 #[must_use] pub fn get_drag_state(&self) -> Option<crate::managers::drag_drop::DragState> {
3680 let ctx = self.get_layout_window().gesture_drag_manager.get_drag_context()?;
3681 crate::managers::drag_drop::DragState::from_context(ctx)
3682 }
3683
3684 #[must_use] pub const fn get_drag_context(&self) -> Option<&azul_core::drag::DragContext> {
3689 self.get_layout_window().gesture_drag_manager.get_drag_context()
3694 }
3695
3696 #[must_use] pub fn get_current_hit_test(&self) -> Option<&FullHitTest> {
3700 self.get_hover_manager().get_current(&InputPointId::Mouse)
3701 }
3702
3703 #[must_use] pub fn get_hit_test_frame(&self, frames_ago: usize) -> Option<&FullHitTest> {
3705 self.get_hover_manager()
3706 .get_frame(&InputPointId::Mouse, frames_ago)
3707 }
3708
3709 #[must_use] pub fn get_hit_test_history(&self) -> Option<&VecDeque<FullHitTest>> {
3713 self.get_hover_manager().get_history(&InputPointId::Mouse)
3714 }
3715
3716 #[must_use] pub fn has_sufficient_history_for_gestures(&self) -> bool {
3718 self.get_hover_manager()
3719 .has_sufficient_history_for_gestures(&InputPointId::Mouse)
3720 }
3721
3722 #[must_use] pub const fn get_file_drop_manager(&self) -> &FileDropManager {
3726 &self.get_layout_window().file_drop_manager
3727 }
3728
3729 #[must_use] pub fn get_dragged_node(&self) -> Option<DomNodeId> {
3734 self.get_drag_context()
3735 .and_then(|ctx| {
3736 ctx.as_node_drag().map(|node_drag| {
3737 DomNodeId {
3738 dom: node_drag.dom_id,
3739 node: NodeHierarchyItemId::from_crate_internal(Some(node_drag.node_id)),
3740 }
3741 })
3742 })
3743 }
3744
3745 #[must_use] pub fn get_dragged_file(&self) -> Option<&AzString> {
3747 self.get_drag_context()
3750 .and_then(|ctx| {
3751 ctx.as_file_drop().and_then(|file_drop| {
3752 file_drop.files.as_ref().first()
3753 })
3754 })
3755 .or_else(|| {
3756 let lw = self.get_layout_window();
3757 lw.file_drop_manager
3758 .get_hovered_files()
3759 .first()
3760 .or_else(|| lw.file_drop_manager.get_dropped_files().first())
3761 })
3762 }
3763
3764 #[must_use] pub fn get_drag_types(&self) -> StringVec {
3769 let lw = self.get_layout_window();
3770 if let Some(ctx) = lw.gesture_drag_manager.get_drag_context() {
3772 if let Some(node_drag) = ctx.as_node_drag() {
3773 return node_drag
3774 .drag_data
3775 .data
3776 .as_ref()
3777 .iter()
3778 .map(|e| e.mime_type.clone())
3779 .collect();
3780 }
3781 }
3782 StringVec::from_const_slice(&[])
3783 }
3784
3785 #[must_use] pub fn get_drag_data(&self, mime_type: &str) -> OptionU8Vec {
3790 let lw = self.get_layout_window();
3791 if let Some(ctx) = lw.gesture_drag_manager.get_drag_context() {
3792 if let Some(node_drag) = ctx.as_node_drag() {
3793 return node_drag.drag_data.get_data(mime_type).map(|d| U8Vec::from(d.to_vec())).into();
3794 }
3795 }
3796 OptionU8Vec::None
3797 }
3798
3799 pub fn set_drag_data(&mut self, mime_type: AzString, data: Vec<u8>) {
3804 self.push_change(CallbackChange::SetDragData { mime_type, data });
3805 }
3806
3807 pub fn accept_drop(&mut self) {
3814 self.push_change(CallbackChange::AcceptDrop);
3815 }
3816
3817 pub fn set_drop_effect(&mut self, effect: azul_core::drag::DropEffect) {
3822 self.push_change(CallbackChange::SetDropEffect { effect });
3823 }
3824
3825 #[must_use] pub fn get_scroll_offset(&self) -> Option<LogicalPosition> {
3832 self.get_scroll_offset_for_node(
3833 self.hit_dom_node.dom,
3834 self.hit_dom_node.node.into_crate_internal()?,
3835 )
3836 }
3837
3838 #[must_use] pub fn get_scroll_offset_for_node(
3840 &self,
3841 dom_id: DomId,
3842 node_id: NodeId,
3843 ) -> Option<LogicalPosition> {
3844 self.get_scroll_manager()
3845 .get_current_offset(dom_id, node_id)
3846 }
3847
3848 #[must_use] pub fn get_scroll_state(&self, dom_id: DomId, node_id: NodeId) -> Option<&AnimatedScrollState> {
3850 self.get_scroll_manager().get_scroll_state(dom_id, node_id)
3851 }
3852
3853 #[must_use] pub fn get_scroll_node_info(
3858 &self,
3859 dom_id: DomId,
3860 node_id: NodeId,
3861 ) -> Option<crate::managers::scroll_state::ScrollNodeInfo> {
3862 self.get_scroll_manager()
3863 .get_scroll_node_info(dom_id, node_id)
3864 }
3865
3866 #[must_use] pub const fn get_scroll_delta(
3877 &self,
3878 _dom_id: DomId,
3879 _node_id: NodeId,
3880 ) -> Option<LogicalPosition> {
3881 self.get_scroll_manager().pending_wheel_event
3882 }
3883
3884 #[must_use] pub const fn had_scroll_activity(
3887 &self,
3888 _dom_id: DomId,
3889 _node_id: NodeId,
3890 ) -> bool {
3891 false
3892 }
3893
3894 #[must_use] pub fn find_scroll_parent(
3899 &self,
3900 dom_id: DomId,
3901 node_id: NodeId,
3902 ) -> Option<NodeId> {
3903 let layout_window = self.get_layout_window();
3904 let layout_results = &layout_window.layout_results;
3905 let lr = layout_results.get(&dom_id)?;
3906 let node_hierarchy: &[azul_core::styled_dom::NodeHierarchyItem] =
3907 lr.styled_dom.node_hierarchy.as_ref();
3908 self.get_scroll_manager()
3909 .find_scroll_parent(dom_id, node_id, node_hierarchy)
3910 }
3911
3912 #[cfg(feature = "std")]
3918 #[must_use] pub fn get_scroll_input_queue(
3919 &self,
3920 ) -> crate::managers::scroll_state::ScrollInputQueue {
3921 self.get_scroll_manager().scroll_input_queue.clone()
3922 }
3923
3924 #[must_use] pub const fn get_gpu_state_manager(&self) -> &GpuStateManager {
3928 &self.get_layout_window().gpu_state_manager
3929 }
3930
3931 #[must_use] pub const fn get_virtual_view_manager(&self) -> &VirtualViewManager {
3935 &self.get_layout_window().virtual_view_manager
3936 }
3937
3938 #[must_use] pub fn inspect_copy_changeset(&self, target: DomNodeId) -> Option<ClipboardContent> {
3946 let layout_window = self.get_layout_window();
3947 let dom_id = &target.dom;
3948 layout_window.get_selected_content_for_clipboard(dom_id)
3949 }
3950
3951 #[must_use] pub fn inspect_cut_changeset(&self, target: DomNodeId) -> Option<ClipboardContent> {
3956 self.inspect_copy_changeset(target)
3958 }
3959
3960 #[must_use] pub fn inspect_paste_target_range(&self, _target: DomNodeId) -> Option<SelectionRange> {
3965 let layout_window = self.get_layout_window();
3966 layout_window
3967 .text_edit_manager.multi_cursor.as_ref()
3968 .and_then(|mc| mc.selections.iter().find_map(|s| match &s.selection {
3969 Selection::Range(r) => Some(*r),
3970 Selection::Cursor(_) => None,
3971 }))
3972 }
3973
3974 #[must_use] pub fn inspect_select_all_changeset(&self, target: DomNodeId) -> Option<SelectAllResult> {
3978 use azul_core::selection::{CursorAffinity, GraphemeClusterId, TextCursor};
3979
3980 let layout_window = self.get_layout_window();
3981 let node_id = target.node.into_crate_internal()?;
3982
3983 let content = layout_window.get_text_before_textinput(target.dom, node_id);
3985 let text = layout_window.extract_text_from_inline_content(&content);
3986
3987 let start_cursor = TextCursor {
3989 cluster_id: GraphemeClusterId {
3990 source_run: 0,
3991 start_byte_in_run: 0,
3992 },
3993 affinity: CursorAffinity::Leading,
3994 };
3995
3996 let end_cursor = TextCursor {
3997 cluster_id: GraphemeClusterId {
3998 source_run: 0,
3999 start_byte_in_run: u32::try_from(text.len()).unwrap_or(u32::MAX),
4000 },
4001 affinity: CursorAffinity::Leading,
4002 };
4003
4004 let range = SelectionRange {
4005 start: start_cursor,
4006 end: end_cursor,
4007 };
4008
4009 Some(SelectAllResult {
4010 full_text: text.into(),
4011 selection_range: range,
4012 })
4013 }
4014
4015 #[must_use] pub fn inspect_delete_changeset(
4024 &self,
4025 target: DomNodeId,
4026 forward: bool,
4027 ) -> Option<DeleteResult> {
4028 let layout_window = self.get_layout_window();
4029 let dom_id = &target.dom;
4030 let node_id = target.node.into_crate_internal()?;
4031
4032 let content = layout_window.get_text_before_textinput(target.dom, node_id);
4034
4035 let selection = if let Some(mc) = layout_window.text_edit_manager.multi_cursor.as_ref() {
4037 if let Some(range) = mc.selections.iter().find_map(|s| match &s.selection {
4038 Selection::Range(r) => Some(*r),
4039 Selection::Cursor(_) => None,
4040 }) {
4041 Selection::Range(range)
4042 } else if let Some(cursor) = mc.get_primary_cursor() {
4043 Selection::Cursor(cursor)
4044 } else {
4045 return None;
4046 }
4047 } else {
4048 return None; };
4050
4051 crate::text3::edit::inspect_delete(&content, &selection, forward).map(|(range, text)| {
4053 DeleteResult {
4054 range_to_delete: range,
4055 deleted_text: text.into(),
4056 }
4057 })
4058 }
4059
4060 #[must_use] pub fn inspect_undo_operation(&self, node_id: NodeId) -> Option<&UndoableOperation> {
4065 self.get_undo_redo_manager().peek_undo(node_id)
4066 }
4067
4068 #[must_use] pub fn inspect_redo_operation(&self, node_id: NodeId) -> Option<&UndoableOperation> {
4072 self.get_undo_redo_manager().peek_redo(node_id)
4073 }
4074
4075 #[must_use] pub fn can_undo(&self, node_id: NodeId) -> bool {
4079 self.get_undo_redo_manager()
4080 .get_stack(node_id)
4081 .is_some_and(super::managers::undo_redo::NodeUndoRedoStack::can_undo)
4082 }
4083
4084 #[must_use] pub fn can_redo(&self, node_id: NodeId) -> bool {
4088 self.get_undo_redo_manager()
4089 .get_stack(node_id)
4090 .is_some_and(super::managers::undo_redo::NodeUndoRedoStack::can_redo)
4091 }
4092
4093 #[must_use] pub fn get_undo_text(&self, node_id: NodeId) -> Option<AzString> {
4098 self.get_undo_redo_manager()
4099 .peek_undo(node_id)
4100 .map(|op| op.pre_state.text_content.clone())
4101 }
4102
4103 #[must_use] pub fn get_redo_text(&self, node_id: NodeId) -> Option<AzString> {
4108 self.get_undo_redo_manager()
4109 .peek_redo(node_id)
4110 .map(|op| op.pre_state.text_content.clone())
4111 }
4112
4113 #[must_use] pub const fn get_clipboard_content(&self) -> Option<&ClipboardContent> {
4126 unsafe {
4127 (*self.ref_data)
4128 .layout_window
4129 .clipboard_manager
4130 .get_paste_content()
4131 }
4132 }
4133
4134 pub fn set_clipboard_content(&mut self, content: ClipboardContent) {
4142 self.set_copy_content(self.hit_dom_node, content);
4143 }
4144
4145 pub fn set_copy_content(&mut self, target: DomNodeId, content: ClipboardContent) {
4151 self.push_change(CallbackChange::SetCopyContent { target, content });
4152 }
4153
4154 pub fn set_cut_content(&mut self, target: DomNodeId, content: ClipboardContent) {
4159 self.push_change(CallbackChange::SetCutContent { target, content });
4160 }
4161
4162 pub fn set_select_all_range(&mut self, target: DomNodeId, range: SelectionRange) {
4167 self.push_change(CallbackChange::SetSelectAllRange { target, range });
4168 }
4169
4170 pub fn request_hit_test_update(&mut self, position: LogicalPosition) {
4178 self.push_change(CallbackChange::RequestHitTestUpdate { position });
4179 }
4180
4181 pub fn process_text_selection_click(&mut self, position: LogicalPosition, time_ms: u64) {
4189 self.push_change(CallbackChange::ProcessTextSelectionClick { position, time_ms });
4190 }
4191
4192 #[must_use] pub fn get_node_text_content(&self, target: DomNodeId) -> Option<String> {
4196 let layout_window = self.get_layout_window();
4197 let node_id = target.node.into_crate_internal()?;
4198 let exists = layout_window.dirty_text_nodes.contains_key(&(target.dom, node_id))
4204 || layout_window
4205 .layout_results
4206 .get(&target.dom)
4207 .is_some_and(|lr| node_id.index() < lr.styled_dom.node_data.as_ref().len());
4208 if !exists {
4209 return None;
4210 }
4211 let content = layout_window.get_text_before_textinput(target.dom, node_id);
4212 Some(layout_window.extract_text_from_inline_content(&content))
4213 }
4214
4215 #[must_use] pub fn get_node_cursor_position(&self, target: DomNodeId) -> Option<TextCursor> {
4219 let layout_window = self.get_layout_window();
4220
4221 if !layout_window.focus_manager.has_focus(&target) {
4223 return None;
4224 }
4225
4226 layout_window.text_edit_manager.get_primary_cursor()
4227 }
4228
4229 #[must_use] pub fn get_node_selection_ranges(&self, _target: DomNodeId) -> SelectionRangeVec {
4233 let layout_window = self.get_layout_window();
4234 let ranges: Vec<SelectionRange> = layout_window
4235 .text_edit_manager.multi_cursor.as_ref()
4236 .map(|mc| mc.selections.iter().filter_map(|s| match &s.selection {
4237 Selection::Range(r) => Some(*r),
4238 Selection::Cursor(_) => None,
4239 }).collect()).unwrap_or_default();
4240 ranges.into()
4241 }
4242
4243 #[must_use] pub fn node_has_selection(&self, target: DomNodeId) -> bool {
4248 !self.get_node_selection_ranges(target).as_ref().is_empty()
4249 }
4250
4251 #[must_use] pub fn get_node_text_length(&self, target: DomNodeId) -> Option<usize> {
4255 self.get_node_text_content(target).map(|text| text.len())
4256 }
4257
4258 pub fn inspect_move_cursor_left(&self, target: DomNodeId) -> Option<TextCursor> {
4268 let layout_window = self.get_layout_window();
4269 let cursor = layout_window.text_edit_manager.get_primary_cursor()?;
4270
4271 let layout = self.get_inline_layout_for_node(&target)?;
4274
4275 let new_cursor = layout.move_cursor_left(cursor, &mut None);
4277
4278 if new_cursor == cursor {
4280 None
4281 } else {
4282 Some(new_cursor)
4283 }
4284 }
4285
4286 pub fn inspect_move_cursor_right(&self, target: DomNodeId) -> Option<TextCursor> {
4291 let layout_window = self.get_layout_window();
4292 let cursor = layout_window.text_edit_manager.get_primary_cursor()?;
4293
4294 let layout = self.get_inline_layout_for_node(&target)?;
4297
4298 let new_cursor = layout.move_cursor_right(cursor, &mut None);
4300
4301 if new_cursor == cursor {
4303 None
4304 } else {
4305 Some(new_cursor)
4306 }
4307 }
4308
4309 pub fn inspect_move_cursor_up(&self, target: DomNodeId) -> Option<TextCursor> {
4314 let layout_window = self.get_layout_window();
4315 let cursor = layout_window.text_edit_manager.get_primary_cursor()?;
4316
4317 let layout = self.get_inline_layout_for_node(&target)?;
4320
4321 let new_cursor = layout.move_cursor_up(cursor, &mut None, &mut None);
4324
4325 if new_cursor == cursor {
4327 None
4328 } else {
4329 Some(new_cursor)
4330 }
4331 }
4332
4333 pub fn inspect_move_cursor_down(&self, target: DomNodeId) -> Option<TextCursor> {
4338 let layout_window = self.get_layout_window();
4339 let cursor = layout_window.text_edit_manager.get_primary_cursor()?;
4340
4341 let layout = self.get_inline_layout_for_node(&target)?;
4344
4345 let new_cursor = layout.move_cursor_down(cursor, &mut None, &mut None);
4348
4349 if new_cursor == cursor {
4351 None
4352 } else {
4353 Some(new_cursor)
4354 }
4355 }
4356
4357 pub fn inspect_move_cursor_to_line_start(&self, target: DomNodeId) -> Option<TextCursor> {
4361 let layout_window = self.get_layout_window();
4362 let cursor = layout_window.text_edit_manager.get_primary_cursor()?;
4363
4364 let layout = self.get_inline_layout_for_node(&target)?;
4367
4368 let new_cursor = layout.move_cursor_to_line_start(cursor, &mut None);
4370
4371 Some(new_cursor)
4373 }
4374
4375 pub fn inspect_move_cursor_to_line_end(&self, target: DomNodeId) -> Option<TextCursor> {
4379 let layout_window = self.get_layout_window();
4380 let cursor = layout_window.text_edit_manager.get_primary_cursor()?;
4381
4382 let layout = self.get_inline_layout_for_node(&target)?;
4385
4386 let new_cursor = layout.move_cursor_to_line_end(cursor, &mut None);
4388
4389 Some(new_cursor)
4391 }
4392
4393 #[must_use] pub const fn inspect_move_cursor_to_document_start(&self, target: DomNodeId) -> Option<TextCursor> {
4397 use azul_core::selection::{CursorAffinity, GraphemeClusterId};
4398
4399 Some(TextCursor {
4400 cluster_id: GraphemeClusterId {
4401 source_run: 0,
4402 start_byte_in_run: 0,
4403 },
4404 affinity: CursorAffinity::Leading,
4405 })
4406 }
4407
4408 #[must_use] pub fn inspect_move_cursor_to_document_end(&self, target: DomNodeId) -> Option<TextCursor> {
4412 use azul_core::selection::{CursorAffinity, GraphemeClusterId};
4413
4414 let text_len = self.get_node_text_length(target)?;
4415
4416 Some(TextCursor {
4417 cluster_id: GraphemeClusterId {
4418 source_run: 0,
4419 start_byte_in_run: u32::try_from(text_len).unwrap_or(u32::MAX),
4420 },
4421 affinity: CursorAffinity::Leading,
4422 })
4423 }
4424
4425 #[must_use] pub fn inspect_backspace(&self, target: DomNodeId) -> Option<DeleteResult> {
4430 self.inspect_delete_changeset(target, false)
4431 }
4432
4433 #[must_use] pub fn inspect_delete(&self, target: DomNodeId) -> Option<DeleteResult> {
4438 self.inspect_delete_changeset(target, true)
4439 }
4440
4441 pub fn move_cursor_left(&mut self, target: DomNodeId, extend_selection: bool) {
4450 self.push_change(CallbackChange::MoveCursorLeft {
4451 dom_id: target.dom,
4452 node_id: target.node.into_crate_internal().unwrap_or(NodeId::ZERO),
4453 extend_selection,
4454 });
4455 }
4456
4457 pub fn move_cursor_right(&mut self, target: DomNodeId, extend_selection: bool) {
4459 self.push_change(CallbackChange::MoveCursorRight {
4460 dom_id: target.dom,
4461 node_id: target.node.into_crate_internal().unwrap_or(NodeId::ZERO),
4462 extend_selection,
4463 });
4464 }
4465
4466 pub fn move_cursor_up(&mut self, target: DomNodeId, extend_selection: bool) {
4468 self.push_change(CallbackChange::MoveCursorUp {
4469 dom_id: target.dom,
4470 node_id: target.node.into_crate_internal().unwrap_or(NodeId::ZERO),
4471 extend_selection,
4472 });
4473 }
4474
4475 pub fn move_cursor_down(&mut self, target: DomNodeId, extend_selection: bool) {
4477 self.push_change(CallbackChange::MoveCursorDown {
4478 dom_id: target.dom,
4479 node_id: target.node.into_crate_internal().unwrap_or(NodeId::ZERO),
4480 extend_selection,
4481 });
4482 }
4483
4484 pub fn move_cursor_to_line_start(&mut self, target: DomNodeId, extend_selection: bool) {
4486 self.push_change(CallbackChange::MoveCursorToLineStart {
4487 dom_id: target.dom,
4488 node_id: target.node.into_crate_internal().unwrap_or(NodeId::ZERO),
4489 extend_selection,
4490 });
4491 }
4492
4493 pub fn move_cursor_to_line_end(&mut self, target: DomNodeId, extend_selection: bool) {
4495 self.push_change(CallbackChange::MoveCursorToLineEnd {
4496 dom_id: target.dom,
4497 node_id: target.node.into_crate_internal().unwrap_or(NodeId::ZERO),
4498 extend_selection,
4499 });
4500 }
4501
4502 pub fn move_cursor_to_document_start(&mut self, target: DomNodeId, extend_selection: bool) {
4504 self.push_change(CallbackChange::MoveCursorToDocumentStart {
4505 dom_id: target.dom,
4506 node_id: target.node.into_crate_internal().unwrap_or(NodeId::ZERO),
4507 extend_selection,
4508 });
4509 }
4510
4511 pub fn move_cursor_to_document_end(&mut self, target: DomNodeId, extend_selection: bool) {
4513 self.push_change(CallbackChange::MoveCursorToDocumentEnd {
4514 dom_id: target.dom,
4515 node_id: target.node.into_crate_internal().unwrap_or(NodeId::ZERO),
4516 extend_selection,
4517 });
4518 }
4519
4520 pub fn delete_backward(&mut self, target: DomNodeId) {
4525 self.push_change(CallbackChange::DeleteBackward {
4526 dom_id: target.dom,
4527 node_id: target.node.into_crate_internal().unwrap_or(NodeId::ZERO),
4528 });
4529 }
4530
4531 pub fn delete_forward(&mut self, target: DomNodeId) {
4536 self.push_change(CallbackChange::DeleteForward {
4537 dom_id: target.dom,
4538 node_id: target.node.into_crate_internal().unwrap_or(NodeId::ZERO),
4539 });
4540 }
4541}
4542
4543#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
4545#[repr(C)]
4546pub struct ExternalSystemCallbacks {
4547 pub create_thread_fn: CreateThreadCallback,
4548 pub get_system_time_fn: GetSystemTimeCallback,
4549}
4550
4551impl ExternalSystemCallbacks {
4552 #[must_use] pub fn rust_internal() -> Self {
4553 use crate::thread::create_thread_libstd;
4554
4555 Self {
4556 create_thread_fn: CreateThreadCallback {
4557 cb: create_thread_libstd,
4558 },
4559 get_system_time_fn: GetSystemTimeCallback {
4560 cb: task::get_system_time_libstd,
4561 },
4562 }
4563 }
4564}
4565
4566#[derive(Copy, Debug, Clone, PartialEq, Eq)]
4568pub enum FocusUpdateRequest {
4569 FocusNode(DomNodeId),
4571 ClearFocus,
4573 NoChange,
4575}
4576
4577impl FocusUpdateRequest {
4578 #[must_use] pub const fn is_change(&self) -> bool {
4580 !matches!(self, Self::NoChange)
4581 }
4582
4583 #[must_use] pub const fn to_focused_node(&self) -> Option<Option<DomNodeId>> {
4585 match self {
4586 Self::FocusNode(node) => Some(Some(*node)),
4587 Self::ClearFocus => Some(None),
4588 Self::NoChange => None,
4589 }
4590 }
4591
4592 #[must_use] pub const fn from_optional(opt: Option<Option<DomNodeId>>) -> Self {
4594 match opt {
4595 Some(Some(node)) => Self::FocusNode(node),
4596 Some(None) => Self::ClearFocus,
4597 None => Self::NoChange,
4598 }
4599 }
4600}
4601
4602#[derive(Debug, Clone, PartialEq, PartialOrd, Hash, Eq, Ord)]
4605#[repr(C)]
4606pub struct MenuCallback {
4607 pub callback: Callback,
4608 pub refany: RefAny,
4609}
4610#[allow(variant_size_differences)] #[derive(Debug, Clone, PartialEq, PartialOrd, Hash, Eq, Ord)]
4613#[repr(C, u8)]
4614pub enum OptionMenuCallback {
4615 None,
4616 Some(MenuCallback),
4617}
4618
4619impl OptionMenuCallback {
4620 #[must_use] pub fn into_option(self) -> Option<MenuCallback> {
4621 match self {
4622 Self::None => None,
4623 Self::Some(c) => Some(c),
4624 }
4625 }
4626
4627 #[must_use] pub const fn is_some(&self) -> bool {
4628 matches!(self, Self::Some(_))
4629 }
4630
4631 #[must_use] pub const fn is_none(&self) -> bool {
4632 matches!(self, Self::None)
4633 }
4634}
4635
4636impl From<Option<MenuCallback>> for OptionMenuCallback {
4637 fn from(o: Option<MenuCallback>) -> Self {
4638 o.map_or_else(|| Self::None, Self::Some)
4639 }
4640}
4641
4642impl From<OptionMenuCallback> for Option<MenuCallback> {
4643 fn from(o: OptionMenuCallback) -> Self {
4644 o.into_option()
4645 }
4646}
4647
4648pub type RenderImageCallbackType = extern "C" fn(RefAny, RenderImageCallbackInfo) -> ImageRef;
4656
4657#[repr(C)]
4664pub struct RenderImageCallback {
4665 pub cb: RenderImageCallbackType,
4666 pub ctx: OptionRefAny,
4669}
4670
4671impl_callback!(RenderImageCallback, RenderImageCallbackType);
4672
4673impl RenderImageCallback {
4674 pub fn create(cb: RenderImageCallbackType) -> Self {
4676 Self {
4677 cb,
4678 ctx: OptionRefAny::None,
4679 }
4680 }
4681
4682 #[must_use] pub fn from_core(core_callback: &azul_core::callbacks::CoreRenderImageCallback) -> Self {
4690 debug_assert!(core_callback.cb != 0, "CoreRenderImageCallback.cb is null");
4691 Self {
4692 cb: unsafe { core::mem::transmute::<usize, RenderImageCallbackType>(core_callback.cb) },
4693 ctx: core_callback.ctx.clone(),
4694 }
4695 }
4696
4697 #[must_use] pub fn to_core(self) -> azul_core::callbacks::CoreRenderImageCallback {
4701 azul_core::callbacks::CoreRenderImageCallback {
4702 cb: self.cb as usize,
4703 ctx: self.ctx,
4704 }
4705 }
4706}
4707
4708impl From<RenderImageCallback> for azul_core::callbacks::CoreRenderImageCallback {
4710 fn from(callback: RenderImageCallback) -> Self {
4711 callback.to_core()
4712 }
4713}
4714
4715#[derive(Debug)]
4717#[repr(C)]
4718pub struct RenderImageCallbackInfo {
4719 callback_node_id: DomNodeId,
4721 bounds: HidpiAdjustedBounds,
4723 gl_context: *const OptionGlContextPtr,
4725 image_cache: *const ImageCache,
4727 system_fonts: *const FcFontCache,
4729 callable_ptr: *const OptionRefAny,
4731 _abi_mut: *mut core::ffi::c_void,
4733}
4734
4735impl Clone for RenderImageCallbackInfo {
4736 #[allow(clippy::used_underscore_binding)]
4738 fn clone(&self) -> Self {
4739 Self {
4740 callback_node_id: self.callback_node_id,
4741 bounds: self.bounds,
4742 gl_context: self.gl_context,
4743 image_cache: self.image_cache,
4744 system_fonts: self.system_fonts,
4745 callable_ptr: self.callable_ptr,
4746 _abi_mut: self._abi_mut,
4747 }
4748 }
4749}
4750
4751impl RenderImageCallbackInfo {
4752 #[must_use] pub const fn new<'a>(
4753 callback_node_id: DomNodeId,
4754 bounds: HidpiAdjustedBounds,
4755 gl_context: &'a OptionGlContextPtr,
4756 image_cache: &'a ImageCache,
4757 system_fonts: &'a FcFontCache,
4758 ) -> Self {
4759 Self {
4760 callback_node_id,
4761 bounds,
4762 gl_context: std::ptr::from_ref::<OptionGlContextPtr>(gl_context),
4763 image_cache: std::ptr::from_ref::<ImageCache>(image_cache),
4764 system_fonts: std::ptr::from_ref::<FcFontCache>(system_fonts),
4765 callable_ptr: core::ptr::null(),
4766 _abi_mut: core::ptr::null_mut(),
4767 }
4768 }
4769
4770 #[must_use] pub fn get_ctx(&self) -> OptionRefAny {
4772 if self.callable_ptr.is_null() {
4773 OptionRefAny::None
4774 } else {
4775 unsafe { (*self.callable_ptr).clone() }
4776 }
4777 }
4778
4779 pub const unsafe fn set_callable_ptr(&mut self, ptr: *const OptionRefAny) {
4790 self.callable_ptr = ptr;
4791 }
4792
4793 #[must_use] pub const fn get_callback_node_id(&self) -> DomNodeId {
4794 self.callback_node_id
4795 }
4796
4797 #[must_use] pub const fn get_bounds(&self) -> HidpiAdjustedBounds {
4798 self.bounds
4799 }
4800
4801 const fn internal_get_gl_context(&self) -> &OptionGlContextPtr {
4802 unsafe { &*self.gl_context }
4803 }
4804
4805 const fn internal_get_image_cache(&self) -> &ImageCache {
4806 unsafe { &*self.image_cache }
4807 }
4808
4809 const fn internal_get_system_fonts(&self) -> &FcFontCache {
4810 unsafe { &*self.system_fonts }
4811 }
4812
4813 #[must_use] pub fn get_gl_context(&self) -> OptionGlContextPtr {
4814 self.internal_get_gl_context().clone()
4815 }
4816}
4817
4818#[derive(Debug, Clone)]
4824#[repr(C, u8)]
4825pub enum ResultU8VecString {
4826 Ok(U8Vec),
4827 Err(AzString),
4828}
4829
4830impl From<Result<alloc::vec::Vec<u8>, AzString>> for ResultU8VecString {
4831 fn from(result: Result<alloc::vec::Vec<u8>, AzString>) -> Self {
4832 match result {
4833 Ok(v) => Self::Ok(v.into()),
4834 Err(e) => Self::Err(e),
4835 }
4836 }
4837}
4838#[allow(variant_size_differences)] #[derive(Debug, Clone)]
4841#[repr(C, u8)]
4842pub enum ResultVoidString {
4843 Ok,
4844 Err(AzString),
4845}
4846
4847impl From<Result<(), AzString>> for ResultVoidString {
4848 fn from(result: Result<(), AzString>) -> Self {
4849 match result {
4850 Ok(()) => Self::Ok,
4851 Err(e) => Self::Err(e),
4852 }
4853 }
4854}
4855
4856#[derive(Debug, Clone)]
4858#[repr(C, u8)]
4859pub enum ResultStringString {
4860 Ok(AzString),
4861 Err(AzString),
4862}
4863
4864impl From<Result<AzString, AzString>> for ResultStringString {
4865 fn from(result: Result<AzString, AzString>) -> Self {
4866 match result {
4867 Ok(s) => Self::Ok(s),
4868 Err(e) => Self::Err(e),
4869 }
4870 }
4871}
4872
4873const BASE64_ALPHABET: &[u8; 64] =
4878 b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
4879
4880#[must_use] pub fn base64_encode(input: &[u8]) -> String {
4882 let mut output = String::with_capacity(input.len().div_ceil(3) * 4);
4883
4884 for chunk in input.chunks(3) {
4885 let b0 = chunk[0] as usize;
4886 let b1 = chunk.get(1).copied().unwrap_or(0) as usize;
4887 let b2 = chunk.get(2).copied().unwrap_or(0) as usize;
4888
4889 let n = (b0 << 16) | (b1 << 8) | b2;
4890
4891 output.push(BASE64_ALPHABET[(n >> 18) & 0x3F] as char);
4892 output.push(BASE64_ALPHABET[(n >> 12) & 0x3F] as char);
4893
4894 if chunk.len() > 1 {
4895 output.push(BASE64_ALPHABET[(n >> 6) & 0x3F] as char);
4896 } else {
4897 output.push('=');
4898 }
4899
4900 if chunk.len() > 2 {
4901 output.push(BASE64_ALPHABET[n & 0x3F] as char);
4902 } else {
4903 output.push('=');
4904 }
4905 }
4906
4907 output
4908}
4909
4910#[cfg(all(test, feature = "std"))]
4911#[allow(clippy::float_cmp, clippy::cast_possible_truncation)]
4912mod autotest_generated {
4913 use super::*;
4914
4915 fn with_info<R>(hit: DomNodeId, f: impl FnOnce(&mut CallbackInfo) -> R) -> R {
4924 let layout_window =
4925 LayoutWindow::new(FcFontCache::default()).expect("LayoutWindow::new failed");
4926 let renderer_resources = RendererResources::default();
4927 let previous_window_state: Option<FullWindowState> = None;
4928 let current_window_state = FullWindowState::default();
4929 let gl_context = OptionGlContextPtr::None;
4930 let scroll_states: BTreeMap<DomId, BTreeMap<NodeHierarchyItemId, ScrollPosition>> =
4931 BTreeMap::new();
4932 let window_handle = RawWindowHandle::Unsupported;
4933 let system_callbacks = ExternalSystemCallbacks::rust_internal();
4934
4935 let ref_data = CallbackInfoRefData {
4936 layout_window: &layout_window,
4937 renderer_resources: &renderer_resources,
4938 previous_window_state: &previous_window_state,
4939 current_window_state: ¤t_window_state,
4940 gl_context: &gl_context,
4941 current_scroll_manager: &scroll_states,
4942 current_window_handle: &window_handle,
4943 system_callbacks: &system_callbacks,
4944 system_style: Arc::new(SystemStyle::default()),
4945 monitors: Arc::new(std::sync::Mutex::new(MonitorVec::from_const_slice(&[]))),
4946 #[cfg(feature = "icu")]
4947 icu_localizer: IcuLocalizerHandle::default(),
4948 ctx: OptionRefAny::None,
4949 };
4950
4951 let changes: Arc<std::sync::Mutex<Vec<CallbackChange>>> =
4952 Arc::new(std::sync::Mutex::new(Vec::new()));
4953
4954 let mut info = CallbackInfo::new(
4955 &ref_data,
4956 &changes,
4957 hit,
4958 OptionLogicalPosition::None,
4959 OptionLogicalPosition::None,
4960 );
4961
4962 f(&mut info)
4963 }
4964
4965 fn node0() -> DomNodeId {
4967 DomNodeId {
4968 dom: DomId::ROOT_ID,
4969 node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(0))),
4970 }
4971 }
4972
4973 fn node_none() -> DomNodeId {
4975 DomNodeId {
4976 dom: DomId::ROOT_ID,
4977 node: NodeHierarchyItemId::NONE,
4978 }
4979 }
4980
4981 extern "C" fn cb_do_nothing(_: RefAny, _: CallbackInfo) -> Update {
4982 Update::DoNothing
4983 }
4984
4985 extern "C" fn cb_refresh_dom(_: RefAny, _: CallbackInfo) -> Update {
4986 Update::RefreshDom
4987 }
4988
4989 extern "C" fn cb_pushes_change(_: RefAny, mut info: CallbackInfo) -> Update {
4991 info.stop_propagation();
4992 Update::RefreshDomAllWindows
4993 }
4994
4995 extern "C" fn img_cb(_: RefAny, _: RenderImageCallbackInfo) -> ImageRef {
4996 ImageRef::null_image(0, 0, azul_core::resources::RawImageFormat::RGBA8, Vec::new())
4997 }
4998
4999 fn a_css_property() -> CssProperty {
5000 use azul_css::props::{basic::PixelValue, layout::dimensions::LayoutWidth};
5001 CssProperty::const_width(LayoutWidth::Px(PixelValue::px(123.0)))
5002 }
5003
5004 fn a_cursor() -> TextCursor {
5005 use azul_core::selection::{CursorAffinity, GraphemeClusterId};
5006 TextCursor {
5007 cluster_id: GraphemeClusterId {
5008 source_run: 0,
5009 start_byte_in_run: 0,
5010 },
5011 affinity: CursorAffinity::Leading,
5012 }
5013 }
5014
5015 fn base64_decode(s: &str) -> Option<Vec<u8>> {
5022 fn val(c: u8) -> Option<u32> {
5023 match c {
5024 b'A'..=b'Z' => Some((c - b'A') as u32),
5025 b'a'..=b'z' => Some((c - b'a') as u32 + 26),
5026 b'0'..=b'9' => Some((c - b'0') as u32 + 52),
5027 b'+' => Some(62),
5028 b'/' => Some(63),
5029 _ => None,
5030 }
5031 }
5032
5033 let bytes = s.as_bytes();
5034 if bytes.len() % 4 != 0 {
5035 return None;
5036 }
5037 let mut out = Vec::with_capacity(bytes.len() / 4 * 3);
5038 for chunk in bytes.chunks(4) {
5039 let pad = chunk.iter().filter(|&&c| c == b'=').count();
5040 if pad > 2 {
5041 return None;
5042 }
5043 let mut n: u32 = 0;
5044 for (i, &c) in chunk.iter().enumerate() {
5045 let v = if c == b'=' { 0 } else { val(c)? };
5046 n |= v << (18 - 6 * i as u32);
5047 }
5048 out.push(((n >> 16) & 0xFF) as u8);
5049 if pad < 2 {
5050 out.push(((n >> 8) & 0xFF) as u8);
5051 }
5052 if pad < 1 {
5053 out.push((n & 0xFF) as u8);
5054 }
5055 }
5056 Some(out)
5057 }
5058
5059 #[test]
5060 fn base64_encode_rfc4648_test_vectors() {
5061 assert_eq!(base64_encode(b""), "");
5062 assert_eq!(base64_encode(b"f"), "Zg==");
5063 assert_eq!(base64_encode(b"fo"), "Zm8=");
5064 assert_eq!(base64_encode(b"foo"), "Zm9v");
5065 assert_eq!(base64_encode(b"foob"), "Zm9vYg==");
5066 assert_eq!(base64_encode(b"fooba"), "Zm9vYmE=");
5067 assert_eq!(base64_encode(b"foobar"), "Zm9vYmFy");
5068 }
5069
5070 #[test]
5071 fn base64_encode_extreme_bytes() {
5072 assert_eq!(base64_encode(&[0x00, 0x00, 0x00]), "AAAA");
5074 assert_eq!(base64_encode(&[0xFF, 0xFF, 0xFF]), "////");
5075 assert_eq!(base64_encode(&[0xFF]), "/w==");
5077 assert_eq!(base64_encode(&[0xFF, 0xFF]), "//8=");
5078 let all: Vec<u8> = (0u8..=255).collect();
5080 let enc = base64_encode(&all);
5081 assert_eq!(base64_decode(&enc).as_deref(), Some(all.as_slice()));
5082 }
5083
5084 #[test]
5085 fn base64_encode_output_length_is_ceil_div_3_times_4() {
5086 for n in 0usize..=64 {
5087 let input = vec![0xABu8; n];
5088 let enc = base64_encode(&input);
5089 assert_eq!(
5090 enc.len(),
5091 n.div_ceil(3) * 4,
5092 "unexpected encoded length for {n} input bytes"
5093 );
5094 let pad = enc.bytes().filter(|&c| c == b'=').count();
5096 assert!(pad <= 2, "too much padding for n = {n}");
5097 assert_eq!(pad, (3 - n % 3) % 3, "wrong padding count for n = {n}");
5098 if pad > 0 {
5099 assert!(enc.ends_with(&"=".repeat(pad)));
5100 }
5101 }
5102 }
5103
5104 #[test]
5105 fn base64_encode_emits_only_alphabet_characters() {
5106 let input: Vec<u8> = (0u8..=255).chain(0u8..=255).collect();
5107 let enc = base64_encode(&input);
5108 for c in enc.bytes() {
5109 assert!(
5110 c == b'=' || BASE64_ALPHABET.contains(&c),
5111 "non-base64 char {c:?} in output"
5112 );
5113 }
5114 }
5115
5116 #[test]
5117 fn base64_encode_round_trips_for_every_length_remainder() {
5118 for n in 0usize..=130 {
5120 let input: Vec<u8> = (0..n).map(|i| (i * 7 + 13) as u8).collect();
5121 let enc = base64_encode(&input);
5122 let dec = base64_decode(&enc).unwrap_or_else(|| panic!("failed to decode {enc:?}"));
5123 assert_eq!(dec, input, "round-trip failed at length {n}");
5124 }
5125 }
5126
5127 #[test]
5128 fn base64_encode_unicode_bytes_round_trip() {
5129 for s in [
5130 "\u{1F600}", "e\u{301}", "\u{0}\u{7F}\u{80}\u{FFFF}", "тест 日本語 🌍",
5134 ] {
5135 let enc = base64_encode(s.as_bytes());
5136 assert_eq!(base64_decode(&enc).as_deref(), Some(s.as_bytes()));
5137 }
5138 assert_eq!(base64_encode("\u{1F600}".as_bytes()), "8J+YgA==");
5140 }
5141
5142 #[test]
5143 fn base64_encode_one_megabyte_does_not_panic_or_hang() {
5144 let input = vec![0x5Au8; 1_000_000];
5145 let enc = base64_encode(&input);
5146 assert_eq!(enc.len(), 1_000_000usize.div_ceil(3) * 4);
5147 assert!(enc.ends_with("=="));
5149 assert_eq!(base64_decode(&enc).map(|v| v.len()), Some(1_000_000));
5150 }
5151
5152 #[test]
5157 fn pen_tilt_from_tuple_preserves_extreme_floats() {
5158 let t = PenTilt::from((0.0, -0.0));
5159 assert_eq!(t.x_tilt, 0.0);
5160 assert!(t.y_tilt.is_sign_negative());
5161
5162 let t = PenTilt::from((f32::MAX, f32::MIN));
5163 assert_eq!(t.x_tilt, f32::MAX);
5164 assert_eq!(t.y_tilt, f32::MIN);
5165
5166 let t = PenTilt::from((f32::INFINITY, f32::NEG_INFINITY));
5167 assert!(t.x_tilt.is_infinite() && t.x_tilt.is_sign_positive());
5168 assert!(t.y_tilt.is_infinite() && t.y_tilt.is_sign_negative());
5169
5170 let t = PenTilt::from((f32::NAN, 90.0));
5173 assert!(t.x_tilt.is_nan());
5174 assert_eq!(t.y_tilt, 90.0);
5175 assert_ne!(t, t);
5176 }
5177
5178 #[test]
5179 fn option_pen_tilt_is_some_is_none_are_exclusive() {
5180 let some = OptionPenTilt::Some(PenTilt::from((1.0, 2.0)));
5181 let none = OptionPenTilt::None;
5182 assert!(some.is_some() && !some.is_none());
5183 assert!(none.is_none() && !none.is_some());
5184 }
5185
5186 #[test]
5187 fn select_all_result_from_tuple_keeps_fields_including_empty_and_huge() {
5188 let range = SelectionRange {
5189 start: a_cursor(),
5190 end: a_cursor(),
5191 };
5192
5193 let empty = SelectAllResult::from((String::new(), range));
5194 assert_eq!(empty.full_text.as_str(), "");
5195 assert_eq!(empty.selection_range, range);
5196
5197 let huge = SelectAllResult::from(("x".repeat(100_000), range));
5198 assert_eq!(huge.full_text.as_str().len(), 100_000);
5199
5200 let unicode = SelectAllResult::from(("🌍\u{0}é".to_string(), range));
5201 assert_eq!(unicode.full_text.as_str(), "🌍\u{0}é");
5202 }
5203
5204 #[test]
5205 fn delete_result_from_tuple_keeps_fields() {
5206 let range = SelectionRange {
5207 start: a_cursor(),
5208 end: a_cursor(),
5209 };
5210 let d = DeleteResult::from((range, String::new()));
5211 assert_eq!(d.range_to_delete, range);
5212 assert_eq!(d.deleted_text.as_str(), "");
5213
5214 let d = DeleteResult::from((range, "\u{1F600}".to_string()));
5215 assert_eq!(d.deleted_text.as_str(), "\u{1F600}");
5216 }
5217
5218 #[test]
5223 fn callback_from_ptr_and_create_and_from_agree() {
5224 let a = Callback::from_ptr(cb_do_nothing);
5225 let b = Callback::create(cb_do_nothing as CallbackType);
5226 let c = Callback::from(cb_do_nothing as CallbackType);
5227
5228 assert_eq!(a, b);
5229 assert_eq!(b, c);
5230 assert!(a.ctx.is_none());
5232 assert!(b.ctx.is_none());
5233 assert!(c.ctx.is_none());
5234 assert_ne!(a.cb as usize, 0);
5235 }
5236
5237 #[test]
5238 fn callback_to_core_from_core_round_trips_pointer_and_ctx() {
5239 let original = Callback {
5240 cb: cb_refresh_dom,
5241 ctx: OptionRefAny::Some(RefAny::new(0xDEAD_BEEFu32)),
5242 };
5243 let ptr = original.cb as usize;
5244
5245 let core = original.to_core();
5246 assert_eq!(core.cb, ptr);
5247 assert!(core.ctx.is_some(), "to_core must not drop the FFI ctx");
5248
5249 let back = Callback::from_core(core);
5250 assert_eq!(back.cb as usize, ptr, "encode == decode for the fn pointer");
5251 assert!(
5252 back.ctx.is_some(),
5253 "from_core must preserve ctx (managed-FFI handlers rely on it)"
5254 );
5255 }
5256
5257 #[test]
5258 fn callback_to_core_of_ctxless_callback_keeps_ctx_none() {
5259 let core = Callback::from_ptr(cb_do_nothing).to_core();
5260 assert!(core.ctx.is_none());
5261 assert_eq!(Callback::from_core(core).cb as usize, cb_do_nothing as usize);
5262 }
5263
5264 #[test]
5265 #[cfg(debug_assertions)]
5266 #[should_panic(expected = "CoreCallback.cb is null")]
5267 fn callback_from_core_null_pointer_trips_debug_assert() {
5268 let _ = Callback::from_core(CoreCallback {
5271 cb: 0,
5272 ctx: OptionRefAny::None,
5273 });
5274 }
5275
5276 #[test]
5277 fn callback_invoke_returns_the_functions_update() {
5278 let update = with_info(node_none(), |info| {
5279 Callback::from_ptr(cb_refresh_dom).invoke(RefAny::new(1u8), *info)
5280 });
5281 assert!(matches!(update, Update::RefreshDom));
5282
5283 let update = with_info(node_none(), |info| {
5284 Callback::from_ptr(cb_do_nothing).invoke(RefAny::new(1u8), *info)
5285 });
5286 assert!(matches!(update, Update::DoNothing));
5287 }
5288
5289 #[test]
5290 fn callback_invoke_changes_reach_the_callers_transaction_log() {
5291 let changes = with_info(node_none(), |info| {
5294 let update = Callback::from_ptr(cb_pushes_change).invoke(RefAny::new(0u8), *info);
5295 assert!(matches!(update, Update::RefreshDomAllWindows));
5296 info.take_changes()
5297 });
5298 assert_eq!(changes.len(), 1);
5299 assert!(matches!(changes[0], CallbackChange::StopPropagation));
5300 }
5301
5302 #[test]
5303 fn callback_eq_and_hash_ignore_ctx_but_stay_consistent() {
5304 use std::{
5305 collections::hash_map::DefaultHasher,
5306 hash::{Hash, Hasher},
5307 };
5308
5309 let plain = Callback::from_ptr(cb_do_nothing);
5310 let with_ctx = Callback {
5311 cb: cb_do_nothing,
5312 ctx: OptionRefAny::Some(RefAny::new(7u64)),
5313 };
5314 let other_fn = Callback::from_ptr(cb_refresh_dom);
5315
5316 assert_eq!(plain, with_ctx);
5318 assert_ne!(plain, other_fn);
5319
5320 let hash = |c: &Callback| {
5322 let mut h = DefaultHasher::new();
5323 c.hash(&mut h);
5324 h.finish()
5325 };
5326 assert_eq!(hash(&plain), hash(&with_ctx));
5327 }
5328
5329 #[test]
5334 fn option_callback_predicates_are_exclusive_and_total() {
5335 let none = OptionCallback::None;
5336 let some = OptionCallback::Some(Callback::from_ptr(cb_do_nothing));
5337
5338 assert!(none.is_none() && !none.is_some());
5339 assert!(some.is_some() && !some.is_none());
5340 for v in [&none, &some] {
5342 assert!(v.is_some() ^ v.is_none());
5343 }
5344 }
5345
5346 #[test]
5347 fn option_callback_round_trips_through_std_option() {
5348 let cb = Callback::from_ptr(cb_do_nothing);
5349
5350 let round = OptionCallback::from(Some(cb.clone())).into_option();
5351 assert_eq!(round, Some(cb.clone()));
5352
5353 let round = OptionCallback::from(None).into_option();
5354 assert_eq!(round, None);
5355
5356 let ffi: OptionCallback = Some(cb.clone()).into();
5358 let back: Option<Callback> = ffi.into();
5359 assert_eq!(back, Some(cb));
5360
5361 let ffi: OptionCallback = None.into();
5362 let back: Option<Callback> = ffi.into();
5363 assert_eq!(back, None);
5364 }
5365
5366 #[test]
5367 fn option_menu_callback_predicates_and_round_trip() {
5368 let mc = MenuCallback {
5369 callback: Callback::from_ptr(cb_do_nothing),
5370 refany: RefAny::new(5i32),
5371 };
5372
5373 let none = OptionMenuCallback::None;
5374 assert!(none.is_none() && !none.is_some());
5375 assert_eq!(none.into_option(), None);
5376
5377 let some = OptionMenuCallback::from(Some(mc.clone()));
5378 assert!(some.is_some() && !some.is_none());
5379 assert_eq!(some.into_option(), Some(mc));
5380
5381 let back: Option<MenuCallback> = OptionMenuCallback::None.into();
5382 assert!(back.is_none());
5383 }
5384
5385 #[test]
5390 fn render_image_callback_core_round_trip() {
5391 let cb = RenderImageCallback::create(img_cb);
5392 assert!(cb.ctx.is_none());
5393 let ptr = cb.cb as usize;
5394
5395 let core = cb.to_core();
5396 assert_eq!(core.cb, ptr);
5397
5398 let back = RenderImageCallback::from_core(&core);
5399 assert_eq!(back.cb as usize, ptr);
5400 assert!(back.ctx.is_none());
5401 }
5402
5403 #[test]
5404 #[cfg(debug_assertions)]
5405 #[should_panic(expected = "CoreRenderImageCallback.cb is null")]
5406 fn render_image_callback_from_core_null_pointer_trips_debug_assert() {
5407 let core = azul_core::callbacks::CoreRenderImageCallback {
5408 cb: 0,
5409 ctx: OptionRefAny::None,
5410 };
5411 let _ = RenderImageCallback::from_core(&core);
5412 }
5413
5414 #[test]
5415 fn render_image_callback_info_getters_and_null_ctx() {
5416 let gl = OptionGlContextPtr::None;
5417 let image_cache = ImageCache::default();
5418 let fonts = FcFontCache::default();
5419 let bounds = HidpiAdjustedBounds {
5420 logical_size: LogicalSize::new(640.0, 480.0),
5421 hidpi_factor: azul_core::resources::DpiScaleFactor::new(2.0),
5422 };
5423
5424 let info = RenderImageCallbackInfo::new(node0(), bounds, &gl, &image_cache, &fonts);
5425
5426 assert_eq!(info.get_callback_node_id(), node0());
5427 assert_eq!(info.get_bounds().logical_size, LogicalSize::new(640.0, 480.0));
5428 assert!(info.get_ctx().is_none());
5431 assert!(info.get_gl_context().is_none());
5432
5433 let cloned = info.clone();
5435 assert_eq!(cloned.get_callback_node_id(), node0());
5436 assert!(cloned.get_ctx().is_none());
5437 }
5438
5439 #[test]
5440 fn render_image_callback_info_accepts_degenerate_and_nan_bounds() {
5441 let gl = OptionGlContextPtr::None;
5442 let image_cache = ImageCache::default();
5443 let fonts = FcFontCache::default();
5444
5445 for (w, h, dpi) in [
5446 (0.0f32, 0.0f32, 0.0f32),
5447 (-1.0, -1.0, 1.0),
5448 (f32::MAX, f32::MAX, f32::MAX),
5449 (f32::INFINITY, f32::NAN, 1.0),
5450 ] {
5451 let bounds = HidpiAdjustedBounds {
5452 logical_size: LogicalSize::new(w, h),
5453 hidpi_factor: azul_core::resources::DpiScaleFactor::new(dpi),
5454 };
5455 let info = RenderImageCallbackInfo::new(node_none(), bounds, &gl, &image_cache, &fonts);
5456 let got = info.get_bounds().logical_size;
5457 assert_eq!(got.width.is_nan(), w.is_nan());
5458 assert_eq!(got.height.is_nan(), h.is_nan());
5459 }
5460 }
5461
5462 #[test]
5463 fn render_image_callback_info_set_callable_ptr_makes_ctx_visible() {
5464 let gl = OptionGlContextPtr::None;
5465 let image_cache = ImageCache::default();
5466 let fonts = FcFontCache::default();
5467 let bounds = HidpiAdjustedBounds {
5468 logical_size: LogicalSize::new(1.0, 1.0),
5469 hidpi_factor: azul_core::resources::DpiScaleFactor::new(1.0),
5470 };
5471 let mut info = RenderImageCallbackInfo::new(node0(), bounds, &gl, &image_cache, &fonts);
5472
5473 let ctx = OptionRefAny::Some(RefAny::new(99u32));
5474 unsafe { info.set_callable_ptr(core::ptr::from_ref(&ctx)) };
5476 assert!(info.get_ctx().is_some());
5477
5478 unsafe { info.set_callable_ptr(core::ptr::null()) };
5480 assert!(info.get_ctx().is_none());
5481 }
5482
5483 #[test]
5488 fn focus_update_request_is_change_matches_variant() {
5489 assert!(FocusUpdateRequest::FocusNode(node0()).is_change());
5490 assert!(FocusUpdateRequest::ClearFocus.is_change());
5491 assert!(!FocusUpdateRequest::NoChange.is_change());
5492 }
5493
5494 #[test]
5495 fn focus_update_request_optional_round_trip_is_lossless() {
5496 for req in [
5497 FocusUpdateRequest::FocusNode(node0()),
5498 FocusUpdateRequest::FocusNode(node_none()),
5499 FocusUpdateRequest::ClearFocus,
5500 FocusUpdateRequest::NoChange,
5501 ] {
5502 assert_eq!(
5503 FocusUpdateRequest::from_optional(req.to_focused_node()),
5504 req,
5505 "from_optional . to_focused_node must be the identity"
5506 );
5507 }
5508
5509 for opt in [Some(Some(node0())), Some(None), None] {
5511 assert_eq!(FocusUpdateRequest::from_optional(opt).to_focused_node(), opt);
5512 }
5513
5514 for req in [
5516 FocusUpdateRequest::FocusNode(node0()),
5517 FocusUpdateRequest::ClearFocus,
5518 FocusUpdateRequest::NoChange,
5519 ] {
5520 assert_eq!(req.is_change(), req.to_focused_node().is_some());
5521 }
5522 }
5523
5524 #[test]
5529 fn result_u8vec_string_from_maps_ok_and_err() {
5530 let ok = ResultU8VecString::from(Ok(Vec::new()));
5531 assert!(matches!(&ok, ResultU8VecString::Ok(v) if v.is_empty()));
5532
5533 let ok = ResultU8VecString::from(Ok(vec![0u8; 100_000]));
5534 assert!(matches!(&ok, ResultU8VecString::Ok(v) if v.len() == 100_000));
5535
5536 let err = ResultU8VecString::from(Err(AzString::from("boom")));
5537 assert!(matches!(&err, ResultU8VecString::Err(e) if e.as_str() == "boom"));
5538 }
5539
5540 #[test]
5541 fn result_void_string_from_maps_ok_and_err() {
5542 assert!(matches!(ResultVoidString::from(Ok(())), ResultVoidString::Ok));
5543 let err = ResultVoidString::from(Err(AzString::from("")));
5544 assert!(matches!(&err, ResultVoidString::Err(e) if e.as_str().is_empty()));
5545 }
5546
5547 #[test]
5548 fn result_string_string_from_keeps_both_sides_distinct() {
5549 let ok = ResultStringString::from(Ok(AzString::from("x")));
5550 assert!(matches!(&ok, ResultStringString::Ok(s) if s.as_str() == "x"));
5551 let err = ResultStringString::from(Err(AzString::from("x")));
5553 assert!(matches!(&err, ResultStringString::Err(s) if s.as_str() == "x"));
5554 }
5555
5556 #[test]
5561 fn external_system_callbacks_time_fn_is_callable_and_monotonic() {
5562 let cbs = ExternalSystemCallbacks::rust_internal();
5563 let t0 = (cbs.get_system_time_fn.cb)();
5564 let t1 = (cbs.get_system_time_fn.cb)();
5565 let _ = (t0, t1);
5568 }
5569
5570 #[test]
5575 fn callback_info_starts_with_an_empty_change_log() {
5576 with_info(node_none(), |info| {
5577 assert!(info.take_changes().is_empty());
5578 assert!(!info.has_pending_relayout_change());
5579 assert!(!info.get_changes_ptr().is_null());
5580 });
5581 }
5582
5583 #[test]
5584 fn callback_info_take_changes_drains_the_log() {
5585 with_info(node_none(), |info| {
5586 info.stop_propagation();
5587 info.prevent_default();
5588 let first = info.take_changes();
5589 assert_eq!(first.len(), 2);
5590 assert!(
5592 info.take_changes().is_empty(),
5593 "take_changes must consume the log"
5594 );
5595 });
5596 }
5597
5598 #[test]
5599 fn callback_info_is_copy_and_copies_share_one_change_log() {
5600 with_info(node_none(), |info| {
5601 let mut copy = *info;
5602 copy.stop_immediate_propagation();
5603 assert_eq!(
5604 info.get_changes_ptr(),
5605 copy.get_changes_ptr(),
5606 "a Copy of CallbackInfo must alias the same Arc<Mutex<..>>"
5607 );
5608 let changes = info.take_changes();
5609 assert_eq!(changes.len(), 1);
5610 assert!(matches!(changes[0], CallbackChange::StopImmediatePropagation));
5611 });
5612 }
5613
5614 #[test]
5615 fn has_pending_relayout_change_is_true_only_for_relayout_changes() {
5616 with_info(node_none(), |info| {
5618 info.stop_propagation();
5619 assert!(!info.has_pending_relayout_change());
5620 });
5621 with_info(node_none(), |info| {
5623 info.modify_window_state(FullWindowState::default());
5624 assert!(info.has_pending_relayout_change());
5625 });
5626 with_info(node_none(), |info| {
5628 info.scroll_to(
5629 DomId::ROOT_ID,
5630 NodeHierarchyItemId::NONE,
5631 LogicalPosition::new(0.0, 0.0),
5632 );
5633 assert!(info.has_pending_relayout_change());
5634 });
5635 with_info(node_none(), |info| {
5637 info.queue_window_state_sequence(FullWindowStateVec::from_vec(vec![
5638 FullWindowState::default(),
5639 ]));
5640 assert!(info.has_pending_relayout_change());
5641 });
5642 with_info(node_none(), |info| {
5644 info.prevent_default();
5645 info.hide_tooltip();
5646 info.close_window();
5647 assert!(!info.has_pending_relayout_change());
5648 info.modify_window_state(FullWindowState::default());
5649 assert!(info.has_pending_relayout_change());
5650 assert!(info.has_pending_relayout_change());
5652 assert_eq!(info.take_changes().len(), 4);
5653 });
5654 }
5655
5656 #[test]
5657 fn callback_info_flag_mutators_queue_exactly_one_matching_change() {
5658 macro_rules! assert_queues {
5659 ($call:expr, $pat:pat) => {{
5660 with_info(node_none(), |info| {
5661 let f: &dyn Fn(&mut CallbackInfo) = &$call;
5662 f(info);
5663 let changes = info.take_changes();
5664 assert_eq!(changes.len(), 1, "expected exactly one queued change");
5665 assert!(
5666 matches!(changes[0], $pat),
5667 "queued the wrong CallbackChange: {:?}",
5668 changes[0]
5669 );
5670 });
5671 }};
5672 }
5673
5674 assert_queues!(
5675 |i: &mut CallbackInfo| i.stop_propagation(),
5676 CallbackChange::StopPropagation
5677 );
5678 assert_queues!(
5679 |i: &mut CallbackInfo| i.stop_immediate_propagation(),
5680 CallbackChange::StopImmediatePropagation
5681 );
5682 assert_queues!(
5683 |i: &mut CallbackInfo| i.prevent_default(),
5684 CallbackChange::PreventDefault
5685 );
5686 assert_queues!(
5687 |i: &mut CallbackInfo| i.close_window(),
5688 CallbackChange::CloseWindow
5689 );
5690 assert_queues!(
5691 |i: &mut CallbackInfo| i.begin_interactive_move(),
5692 CallbackChange::BeginInteractiveMove
5693 );
5694 assert_queues!(
5695 |i: &mut CallbackInfo| i.commit_undo_snapshot(),
5696 CallbackChange::CommitUndoSnapshot
5697 );
5698 assert_queues!(
5699 |i: &mut CallbackInfo| i.undo_app_state(),
5700 CallbackChange::UndoAppState
5701 );
5702 assert_queues!(
5703 |i: &mut CallbackInfo| i.redo_app_state(),
5704 CallbackChange::RedoAppState
5705 );
5706 assert_queues!(
5707 |i: &mut CallbackInfo| i.update_all_image_callbacks(),
5708 CallbackChange::UpdateAllImageCallbacks
5709 );
5710 assert_queues!(
5711 |i: &mut CallbackInfo| i.trigger_all_virtual_view_rerender(),
5712 CallbackChange::UpdateAllVirtualViews
5713 );
5714 assert_queues!(
5715 |i: &mut CallbackInfo| i.reload_system_fonts(),
5716 CallbackChange::ReloadSystemFonts
5717 );
5718 assert_queues!(
5719 |i: &mut CallbackInfo| i.hide_tooltip(),
5720 CallbackChange::HideTooltip
5721 );
5722 }
5723
5724 #[test]
5725 fn callback_info_timer_and_thread_ids_survive_boundary_values() {
5726 with_info(node_none(), |info| {
5727 info.add_timer(TimerId { id: 0 }, Timer::default());
5728 info.add_timer(TimerId { id: usize::MAX }, Timer::default());
5729 info.remove_timer(TimerId { id: usize::MAX });
5730 info.remove_thread(ThreadId::unique());
5731
5732 let changes = info.take_changes();
5733 assert_eq!(changes.len(), 4);
5734 assert!(
5735 matches!(&changes[1], CallbackChange::AddTimer { timer_id, .. } if timer_id.id == usize::MAX)
5736 );
5737 assert!(
5738 matches!(&changes[2], CallbackChange::RemoveTimer { timer_id } if timer_id.id == usize::MAX)
5739 );
5740 assert!(matches!(&changes[3], CallbackChange::RemoveThread { .. }));
5741 });
5742 }
5743
5744 #[test]
5749 fn scroll_to_records_position_verbatim_at_numeric_extremes() {
5750 let positions = [
5751 LogicalPosition::new(0.0, 0.0),
5752 LogicalPosition::new(-0.0, -1_000_000.0),
5753 LogicalPosition::new(f32::MIN, f32::MAX),
5754 LogicalPosition::new(f32::INFINITY, f32::NEG_INFINITY),
5755 LogicalPosition::new(f32::NAN, f32::NAN),
5756 ];
5757
5758 with_info(node_none(), |info| {
5759 for p in positions {
5760 info.scroll_to(DomId::ROOT_ID, NodeHierarchyItemId::NONE, p);
5761 }
5762 let changes = info.take_changes();
5763 assert_eq!(changes.len(), positions.len());
5764
5765 for (change, expected) in changes.iter().zip(positions) {
5766 let CallbackChange::ScrollTo {
5767 position, unclamped, ..
5768 } = change
5769 else {
5770 panic!("expected ScrollTo, got {change:?}");
5771 };
5772 assert!(!*unclamped, "scroll_to must request clamping");
5773 assert_eq!(position.x.is_nan(), expected.x.is_nan());
5776 if !expected.x.is_nan() {
5777 assert_eq!(position.x, expected.x);
5778 assert_eq!(position.y, expected.y);
5779 }
5780 }
5781 });
5782 }
5783
5784 #[test]
5785 fn scroll_to_unclamped_sets_the_unclamped_flag() {
5786 with_info(node_none(), |info| {
5787 info.scroll_to_unclamped(
5788 DomId { inner: usize::MAX },
5789 NodeHierarchyItemId::from_raw(usize::MAX),
5790 LogicalPosition::new(-99999.0, 99999.0),
5791 );
5792 let changes = info.take_changes();
5793 assert_eq!(changes.len(), 1);
5794 let CallbackChange::ScrollTo {
5795 unclamped,
5796 dom_id,
5797 position,
5798 ..
5799 } = &changes[0]
5800 else {
5801 panic!("expected ScrollTo");
5802 };
5803 assert!(*unclamped, "scroll_to_unclamped must skip clamping");
5804 assert_eq!(dom_id.inner, usize::MAX, "an unknown DomId is not rejected here");
5805 assert_eq!(position.x, -99999.0);
5806 });
5807 }
5808
5809 #[test]
5810 fn scroll_node_into_view_queues_the_options_verbatim() {
5811 use crate::managers::scroll_into_view::ScrollIntoViewOptions;
5812 with_info(node_none(), |info| {
5813 info.scroll_node_into_view(node_none(), ScrollIntoViewOptions::nearest());
5814 let changes = info.take_changes();
5815 assert_eq!(changes.len(), 1);
5816 assert!(matches!(changes[0], CallbackChange::ScrollIntoView { .. }));
5817 });
5818 }
5819
5820 #[test]
5821 fn open_menu_at_and_show_tooltip_at_accept_extreme_positions() {
5822 let menu = || Menu::create(azul_core::menu::MenuItemVec::from_const_slice(&[]));
5823
5824 with_info(node_none(), |info| {
5825 info.open_menu(menu());
5826 info.open_menu_at(menu(), LogicalPosition::new(0.0, 0.0));
5827 info.open_menu_at(menu(), LogicalPosition::new(f32::MIN, f32::MAX));
5828 info.open_menu_at(menu(), LogicalPosition::new(f32::NAN, f32::INFINITY));
5829
5830 let changes = info.take_changes();
5831 assert_eq!(changes.len(), 4);
5832 assert!(matches!(
5834 &changes[0],
5835 CallbackChange::OpenMenu { position: None, .. }
5836 ));
5837 for change in &changes[1..] {
5839 assert!(matches!(
5840 change,
5841 CallbackChange::OpenMenu {
5842 position: Some(_),
5843 ..
5844 }
5845 ));
5846 }
5847 });
5848
5849 with_info(node_none(), |info| {
5850 info.show_tooltip(AzString::from(""));
5851 info.show_tooltip_at(AzString::from("🌍"), LogicalPosition::new(f32::NAN, -0.0));
5852 info.show_tooltip_at(
5853 AzString::from("x".repeat(100_000)),
5854 LogicalPosition::new(f32::MAX, f32::MIN),
5855 );
5856 let changes = info.take_changes();
5857 assert_eq!(changes.len(), 3);
5858 assert!(matches!(&changes[0], CallbackChange::ShowTooltip { text, .. } if text.as_str().is_empty()));
5859 assert!(matches!(&changes[1], CallbackChange::ShowTooltip { text, position } if text.as_str() == "🌍" && position.x.is_nan()));
5860 assert!(matches!(&changes[2], CallbackChange::ShowTooltip { text, .. } if text.as_str().len() == 100_000));
5861 });
5862 }
5863
5864 #[test]
5869 fn set_css_property_wraps_a_single_property() {
5870 with_info(node_none(), |info| {
5871 info.set_css_property(node0(), a_css_property());
5872 let changes = info.take_changes();
5873 assert_eq!(changes.len(), 1);
5874 let CallbackChange::ChangeNodeCssProperties {
5875 dom_id,
5876 node_id,
5877 properties,
5878 } = &changes[0]
5879 else {
5880 panic!("expected ChangeNodeCssProperties");
5881 };
5882 assert_eq!(*dom_id, DomId::ROOT_ID);
5883 assert_eq!(node_id.index(), 0);
5884 assert_eq!(properties.len(), 1);
5885 });
5886 }
5887
5888 #[test]
5889 fn override_css_property_uses_the_override_channel_not_the_cascade() {
5890 with_info(node_none(), |info| {
5891 info.override_css_property(node0(), a_css_property());
5892 let changes = info.take_changes();
5893 assert_eq!(changes.len(), 1);
5894 assert!(
5895 matches!(changes[0], CallbackChange::OverrideNodeCssProperties { .. }),
5896 "must not fall back to the invalidating ChangeNodeCssProperties path"
5897 );
5898 });
5899 }
5900
5901 #[test]
5902 #[should_panic(expected = "DomNodeId node should not be None")]
5903 fn set_css_property_panics_on_a_none_node_as_documented() {
5904 with_info(node_none(), |info| {
5905 info.set_css_property(node_none(), a_css_property());
5906 });
5907 }
5908
5909 #[test]
5910 #[should_panic(expected = "DomNodeId node should not be None")]
5911 fn override_css_property_panics_on_a_none_node_as_documented() {
5912 with_info(node_none(), |info| {
5913 info.override_css_property(node_none(), a_css_property());
5914 });
5915 }
5916
5917 #[test]
5918 fn change_node_css_properties_accepts_an_empty_property_vec() {
5919 with_info(node_none(), |info| {
5920 info.change_node_css_properties(
5921 DomId::ROOT_ID,
5922 NodeId::new(usize::MAX),
5923 CssPropertyVec::from_const_slice(&[]),
5924 );
5925 let changes = info.take_changes();
5926 assert_eq!(changes.len(), 1);
5927 assert!(
5928 matches!(&changes[0], CallbackChange::ChangeNodeCssProperties { properties, .. } if properties.is_empty())
5929 );
5930 });
5931 }
5932
5933 #[test]
5938 fn change_node_text_passes_hostile_strings_through_unchanged() {
5939 let inputs = [
5940 String::new(),
5941 " \t\n ".to_string(),
5942 "\u{0}embedded nul".to_string(),
5943 "🌍é\u{301}\u{200B}".to_string(),
5944 "x".repeat(1_000_000),
5945 ];
5946
5947 with_info(node_none(), |info| {
5948 for s in &inputs {
5949 info.change_node_text(node0(), AzString::from(s.clone()));
5950 }
5951 let changes = info.take_changes();
5952 assert_eq!(changes.len(), inputs.len());
5953 for (change, expected) in changes.iter().zip(&inputs) {
5954 let CallbackChange::ChangeNodeText { text, .. } = change else {
5955 panic!("expected ChangeNodeText");
5956 };
5957 assert_eq!(text.as_str(), expected.as_str());
5958 }
5959 });
5960 }
5961
5962 #[test]
5963 fn insert_child_node_accepts_empty_and_garbage_type_strings() {
5964 with_info(node_none(), |info| {
5965 info.insert_child_node(
5967 DomId::ROOT_ID,
5968 NodeId::new(0),
5969 AzString::from(""),
5970 OptionUsize::None,
5971 StringVec::from_const_slice(&[]),
5972 OptionString::None,
5973 );
5974 info.insert_child_node(
5975 DomId { inner: usize::MAX },
5976 NodeId::new(usize::MAX),
5977 AzString::from("\u{0}<<not a tag>>"),
5978 OptionUsize::Some(usize::MAX),
5979 StringVec::from_const_slice(&[]),
5980 OptionString::None,
5981 );
5982 assert_eq!(info.take_changes().len(), 2);
5983 });
5984 }
5985
5986 #[test]
5987 fn text_editing_mutators_queue_their_changes() {
5988 with_info(node_none(), |info| {
5989 info.insert_text(DomId::ROOT_ID, NodeId::new(0), AzString::from("🌍"));
5990 info.move_cursor(DomId::ROOT_ID, NodeId::new(0), a_cursor());
5991 info.set_selection(
5992 DomId::ROOT_ID,
5993 NodeId::new(0),
5994 Selection::Cursor(a_cursor()),
5995 );
5996 info.set_text_changeset(PendingTextEdit {
5997 node: node0(),
5998 inserted_text: AzString::from(""),
5999 old_text: AzString::from(""),
6000 });
6001 info.create_text_input(AzString::from("\u{0}"));
6002 info.delete_node(DomId::ROOT_ID, NodeId::new(usize::MAX));
6003 info.set_node_ids_and_classes(
6004 DomId::ROOT_ID,
6005 NodeId::new(0),
6006 azul_core::dom::IdOrClassVec::from_const_slice(&[]),
6007 );
6008
6009 let changes = info.take_changes();
6010 assert_eq!(changes.len(), 7);
6011 assert!(matches!(&changes[0], CallbackChange::InsertText { text, .. } if text.as_str() == "🌍"));
6012 assert!(matches!(changes[1], CallbackChange::MoveCursor { .. }));
6013 assert!(matches!(changes[2], CallbackChange::SetSelection { .. }));
6014 assert!(matches!(changes[3], CallbackChange::SetTextChangeset { .. }));
6015 assert!(matches!(changes[5], CallbackChange::DeleteNode { .. }));
6016 });
6017 }
6018
6019 #[test]
6020 fn image_cache_mutators_accept_empty_ids_and_null_images() {
6021 with_info(node_none(), |info| {
6022 let img = || {
6023 ImageRef::null_image(0, 0, azul_core::resources::RawImageFormat::RGBA8, Vec::new())
6024 };
6025 info.add_image_to_cache(AzString::from(""), img());
6026 info.remove_image_from_cache(AzString::from(""));
6027 info.change_node_image(
6028 DomId::ROOT_ID,
6029 NodeId::new(0),
6030 img(),
6031 UpdateImageType::Content,
6032 );
6033 info.update_image_callback(DomId { inner: usize::MAX }, NodeId::new(usize::MAX));
6034 info.trigger_virtual_view_rerender(DomId::ROOT_ID, NodeId::new(usize::MAX));
6035 assert_eq!(info.take_changes().len(), 5);
6036 });
6037 }
6038
6039 #[test]
6040 fn focus_mutators_queue_set_focus_target() {
6041 with_info(node_none(), |info| {
6042 info.set_focus(FocusTarget::NoFocus);
6043 info.set_focus_to_node(DomId::ROOT_ID, NodeId::new(usize::MAX - 1));
6047 info.focus_next();
6048 info.focus_previous();
6049 info.focus_first();
6050 info.focus_last();
6051 info.clear_focus();
6052
6053 let changes = info.take_changes();
6054 assert_eq!(changes.len(), 7);
6055 for change in &changes {
6056 assert!(matches!(change, CallbackChange::SetFocusTarget { .. }));
6057 }
6058 assert!(matches!(
6059 &changes[2],
6060 CallbackChange::SetFocusTarget {
6061 target: FocusTarget::Next
6062 }
6063 ));
6064 assert!(matches!(
6065 &changes[6],
6066 CallbackChange::SetFocusTarget {
6067 target: FocusTarget::NoFocus
6068 }
6069 ));
6070 });
6071 }
6072
6073 #[test]
6074 fn create_window_queues_window_creation() {
6075 with_info(node_none(), |info| {
6076 info.create_window(WindowCreateOptions::default());
6077 let changes = info.take_changes();
6078 assert_eq!(changes.len(), 1);
6079 assert!(matches!(changes[0], CallbackChange::CreateNewWindow { .. }));
6080 });
6081 }
6082
6083 #[test]
6088 fn route_getters_return_empty_strings_when_no_route_is_active() {
6089 with_info(node_none(), |info| {
6090 assert_eq!(info.get_route_pattern().as_str(), "");
6091 assert_eq!(info.get_route_param(AzString::from("id")).as_str(), "");
6092 assert_eq!(info.get_route_param(AzString::from("")).as_str(), "");
6094 assert_eq!(info.get_route_param(AzString::from("\u{0}🌍")).as_str(), "");
6095 assert_eq!(
6096 info.get_route_param(AzString::from("k".repeat(100_000)))
6097 .as_str(),
6098 ""
6099 );
6100 });
6101 }
6102
6103 #[test]
6104 fn set_route_param_without_an_active_route_queues_nothing() {
6105 with_info(node_none(), |info| {
6106 info.set_route_param(AzString::from("id"), AzString::from("42"));
6107 assert!(
6108 info.take_changes().is_empty(),
6109 "no active route => no SwitchRoute change may be queued"
6110 );
6111 });
6112 }
6113
6114 #[test]
6115 fn switch_route_queues_the_pattern_verbatim() {
6116 with_info(node_none(), |info| {
6117 info.switch_route(
6118 AzString::from("/user/:id"),
6119 azul_core::window::StringPairVec::from_vec(vec![azul_core::window::AzStringPair {
6120 key: AzString::from("id"),
6121 value: AzString::from("42"),
6122 }]),
6123 );
6124 let changes = info.take_changes();
6125 assert_eq!(changes.len(), 1);
6126 assert!(
6127 matches!(&changes[0], CallbackChange::SwitchRoute { pattern, params } if pattern.as_str() == "/user/:id" && params.len() == 1)
6128 );
6129 });
6130 }
6131
6132 #[test]
6137 fn get_node_id_by_id_attribute_returns_none_for_hostile_ids() {
6138 let long = "a".repeat(1_000_000);
6139 let nested = "[".repeat(10_000);
6140 let ids: [&str; 12] = [
6141 "",
6142 " ",
6143 "\t\n",
6144 "\u{0}",
6145 "!@#$%^&*()",
6146 "0",
6147 "-0",
6148 "9223372036854775807",
6149 "NaN",
6150 "inf",
6151 " valid ",
6152 "valid;garbage",
6153 ];
6154
6155 with_info(node_none(), |info| {
6156 for id in ids {
6157 assert_eq!(
6158 info.get_node_id_by_id_attribute(DomId::ROOT_ID, id),
6159 None,
6160 "id {id:?} must not resolve in an empty layout tree"
6161 );
6162 }
6163 for id in ["\u{1F600}", "e\u{301}", "🌍🌍🌍"] {
6165 assert_eq!(info.get_node_id_by_id_attribute(DomId::ROOT_ID, id), None);
6166 }
6167 assert_eq!(
6169 info.get_node_id_by_id_attribute(DomId::ROOT_ID, &long),
6170 None
6171 );
6172 assert_eq!(
6173 info.get_node_id_by_id_attribute(DomId::ROOT_ID, &nested),
6174 None
6175 );
6176 assert_eq!(
6178 info.get_node_id_by_id_attribute(DomId { inner: usize::MAX }, "x"),
6179 None
6180 );
6181 });
6182 }
6183
6184 #[test]
6185 fn hierarchy_navigation_is_none_and_zero_on_an_empty_layout_tree() {
6186 with_info(node_none(), |info| {
6187 for dom in [DomId::ROOT_ID, DomId { inner: usize::MAX }] {
6188 for node in [NodeId::new(0), NodeId::new(usize::MAX)] {
6189 assert_eq!(info.get_parent_node(dom, node), None);
6190 assert_eq!(info.get_next_sibling_node(dom, node), None);
6191 assert_eq!(info.get_previous_sibling_node(dom, node), None);
6192 assert_eq!(info.get_first_child_node(dom, node), None);
6193 assert_eq!(info.get_last_child_node(dom, node), None);
6194 assert_eq!(info.get_children_count(dom, node), 0);
6195 assert_eq!(info.get_all_children_nodes(dom, node).len(), 0);
6196 }
6197 }
6198 assert_eq!(info.get_parent(node0()), None);
6200 assert_eq!(info.get_first_child(node0()), None);
6201 assert_eq!(info.get_last_child(node0()), None);
6202 assert_eq!(info.get_next_sibling(node_none()), None);
6203 assert_eq!(info.get_previous_sibling(node_none()), None);
6204 });
6205 }
6206
6207 #[test]
6208 fn geometry_and_css_queries_are_none_on_an_empty_layout_tree() {
6209 with_info(node0(), |info| {
6210 assert_eq!(info.get_node_size(node0()), None);
6211 assert_eq!(info.get_node_position(node0()), None);
6212 assert_eq!(info.get_node_rect(node0()), None);
6213 assert_eq!(info.get_node_hit_test_bounds(node0()), None);
6214 assert_eq!(info.get_hit_node_rect(), None);
6215 assert!(info.get_computed_width(node0()).is_none());
6216 assert!(info.get_computed_height(node0()).is_none());
6217 assert!(info
6218 .get_computed_css_property(node_none(), CssPropertyType::Width)
6219 .is_none());
6220 assert!(info.get_layout_result(&DomId::ROOT_ID).is_none());
6221 assert!(info.get_gpu_cache(&DomId::ROOT_ID).is_none());
6222 assert_eq!(info.get_dom_ids().len(), 0);
6223 });
6224 }
6225
6226 #[test]
6227 fn state_getters_reflect_the_construction_arguments() {
6228 let hit = node0();
6229 with_info(hit, |info| {
6230 assert_eq!(info.get_hit_node(), hit);
6231 assert!(info.get_cursor_relative_to_viewport().is_none());
6233 assert!(info.get_cursor_relative_to_node().is_none());
6234 assert!(info.get_ctx().is_none());
6236 assert!(info.get_gl_context().is_none());
6237 assert!(info.get_previous_window_state().is_none());
6239 assert!(info.get_previous_window_flags().is_none());
6240 assert!(info.get_previous_mouse_state().is_none());
6241 assert!(info.get_previous_keyboard_state().is_none());
6242 assert!(matches!(
6243 info.get_current_window_handle(),
6244 RawWindowHandle::Unsupported
6245 ));
6246 assert_eq!(info.get_monitors().len(), 0);
6247 assert!(info.get_current_monitor().is_none());
6248 assert_eq!(info.get_timer_ids().len(), 0);
6249 assert_eq!(info.get_thread_ids().len(), 0);
6250 assert!(info.get_timer(&TimerId { id: 0 }).is_none());
6251 assert!(info.get_thread(&ThreadId::unique()).is_none());
6252 let _now = info.get_current_time();
6254 });
6255 }
6256
6257 #[test]
6258 fn selection_and_undo_queries_are_empty_for_unknown_nodes() {
6259 with_info(node_none(), |info| {
6260 assert!(!info.has_any_selection());
6261 assert_eq!(info.get_selection_count(&DomId::ROOT_ID), 0);
6262 assert!(info.get_primary_selection(&DomId::ROOT_ID).is_none());
6263 assert!(!info.node_has_selection(node0()));
6264
6265 for node in [NodeId::new(0), NodeId::new(usize::MAX)] {
6266 assert!(!info.can_undo(node));
6267 assert!(!info.can_redo(node));
6268 assert!(info.get_undo_text(node).is_none());
6269 assert!(info.get_redo_text(node).is_none());
6270 assert!(info.inspect_undo_operation(node).is_none());
6271 assert!(info.inspect_redo_operation(node).is_none());
6272 }
6273
6274 assert!(info.get_node_text_content(node0()).is_none());
6275 assert_eq!(info.get_node_text_length(node0()), None);
6276 assert!(info.get_text_changeset().is_none());
6277 assert!(!info.is_node_focused(node0()));
6278 assert!(!info.has_focus(node0()));
6279 assert!(info.get_focused_node().is_none());
6280 });
6281 }
6282
6283 #[test]
6284 fn cursor_inspection_is_none_without_a_text_layout() {
6285 with_info(node_none(), |info| {
6286 assert!(info.inspect_move_cursor_left(node0()).is_none());
6287 assert!(info.inspect_move_cursor_right(node0()).is_none());
6288 assert!(info.inspect_move_cursor_up(node0()).is_none());
6289 assert!(info.inspect_move_cursor_down(node0()).is_none());
6290 assert!(info.inspect_move_cursor_to_line_start(node0()).is_none());
6291 assert!(info.inspect_move_cursor_to_line_end(node0()).is_none());
6292 assert!(info.inspect_backspace(node0()).is_none());
6293 assert!(info.inspect_delete(node0()).is_none());
6294 assert!(info.inspect_move_cursor_left(node_none()).is_none());
6296 assert!(info.inspect_backspace(node_none()).is_none());
6297 });
6298 }
6299
6300 #[test]
6301 fn drag_and_gesture_queries_are_inactive_by_default() {
6302 with_info(node_none(), |info| {
6303 assert!(!info.is_dragging());
6304 assert!(!info.is_drag_active());
6305 assert!(!info.is_node_drag_active());
6306 assert!(!info.is_file_drag_active());
6307 assert!(info.get_drag_delta().is_none());
6308 assert!(info.get_drag_delta_screen().is_none());
6309 assert!(info.get_drag_delta_screen_incremental().is_none());
6310 assert!(!info.was_double_clicked());
6311 assert!(info.get_pen_pressure().is_none());
6312 assert!(info.get_pen_tilt().is_none());
6313 assert!(!info.is_pen_in_contact());
6314 assert!(!info.is_pen_eraser());
6315 assert!(!info.is_pen_barrel_button_pressed());
6316 assert_eq!(info.get_drag_types().len(), 0);
6317 assert!(info.get_drag_data("text/plain").is_none());
6318 assert!(info.get_drag_data("").is_none());
6319 });
6320 }
6321
6322 #[test]
6323 #[cfg(feature = "text_layout")]
6324 fn get_loaded_font_bytes_returns_none_for_boundary_hashes() {
6325 with_info(node_none(), |info| {
6326 for hash in [0u64, 1, u64::MAX, u64::MAX / 2] {
6329 assert!(info.get_loaded_font_bytes(hash).is_none());
6330 }
6331 assert_eq!(info.get_loaded_fonts().len(), 0);
6332 });
6333 }
6334
6335 #[test]
6336 #[cfg(feature = "cpurender")]
6337 fn take_screenshot_of_a_missing_dom_is_an_error_not_a_panic() {
6338 with_info(node_none(), |info| {
6339 let err = info
6340 .take_screenshot(DomId::ROOT_ID)
6341 .expect_err("an empty layout window has no DOM to screenshot");
6342 assert_eq!(err.as_str(), "DOM not found in layout results");
6343
6344 let err = info
6345 .take_screenshot(DomId { inner: usize::MAX })
6346 .expect_err("an out-of-range DomId must be rejected");
6347 assert_eq!(err.as_str(), "DOM not found in layout results");
6348
6349 assert!(info.take_screenshot_base64(DomId::ROOT_ID).is_err());
6350 });
6351 }
6352
6353 #[test]
6358 fn callback_change_is_debug_and_clone() {
6359 let change = CallbackChange::ScrollTo {
6360 dom_id: DomId::ROOT_ID,
6361 node_id: NodeHierarchyItemId::NONE,
6362 position: LogicalPosition::new(f32::NAN, 0.0),
6363 unclamped: true,
6364 };
6365 let cloned = change.clone();
6366 assert!(matches!(
6367 cloned,
6368 CallbackChange::ScrollTo { unclamped: true, .. }
6369 ));
6370 assert!(!format!("{change:?}").is_empty());
6371 }
6372}