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::{AccessibilityAction, 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 PerformAccessibilityAction {
189 dom_id: DomId,
190 node_id: NodeId,
191 action: AccessibilityAction,
192 },
193 QueueWindowStateSequence { states: Vec<FullWindowState> },
197 CreateNewWindow { options: WindowCreateOptions },
199 CloseWindow,
201
202 SetFocusTarget { target: FocusTarget },
205
206 StopPropagation,
211 StopImmediatePropagation,
214 PreventDefault,
216
217 AddTimer { timer_id: TimerId, timer: Timer },
220 RemoveTimer { timer_id: TimerId },
222
223 AddThread { thread_id: ThreadId, thread: Thread },
226 RemoveThread { thread_id: ThreadId },
228
229 ChangeNodeText { node_id: DomNodeId, text: AzString },
232 ChangeNodeImage {
234 dom_id: DomId,
235 node_id: NodeId,
236 image: ImageRef,
237 update_type: UpdateImageType,
238 },
239 UpdateImageCallback { dom_id: DomId, node_id: NodeId },
242 UpdateAllImageCallbacks,
249 UpdateVirtualView { dom_id: DomId, node_id: NodeId },
252 UpdateAllVirtualViews,
256 ChangeNodeImageMask {
258 dom_id: DomId,
259 node_id: NodeId,
260 mask: ImageMask,
261 },
262 ChangeNodeCssProperties {
264 dom_id: DomId,
265 node_id: NodeId,
266 properties: CssPropertyVec,
267 },
268 OverrideNodeCssProperties {
276 dom_id: DomId,
277 node_id: NodeId,
278 properties: CssPropertyVec,
279 },
280
281 ScrollTo {
284 dom_id: DomId,
285 node_id: NodeHierarchyItemId,
286 position: LogicalPosition,
287 unclamped: bool,
290 },
291 ScrollIntoView {
294 node_id: DomNodeId,
295 options: crate::managers::scroll_into_view::ScrollIntoViewOptions,
296 },
297
298 AddImageToCache { id: AzString, image: ImageRef },
301 RemoveImageFromCache { id: AzString },
303
304 ReloadSystemFonts,
307
308 OpenMenu {
312 menu: Menu,
313 position: Option<LogicalPosition>,
315 },
316
317 ShowTooltip {
326 text: AzString,
327 position: LogicalPosition,
328 },
329 HideTooltip,
331
332 InsertText {
335 dom_id: DomId,
336 node_id: NodeId,
337 text: AzString,
338 },
339 DeleteBackward { dom_id: DomId, node_id: NodeId },
341 DeleteForward { dom_id: DomId, node_id: NodeId },
343 MoveCursor {
345 dom_id: DomId,
346 node_id: NodeId,
347 cursor: TextCursor,
348 },
349 SetSelection {
351 dom_id: DomId,
352 node_id: NodeId,
353 selection: Selection,
354 },
355 SetTextChangeset { changeset: PendingTextEdit },
358
359 MoveCursorLeft {
362 dom_id: DomId,
363 node_id: NodeId,
364 extend_selection: bool,
365 },
366 MoveCursorRight {
368 dom_id: DomId,
369 node_id: NodeId,
370 extend_selection: bool,
371 },
372 MoveCursorUp {
374 dom_id: DomId,
375 node_id: NodeId,
376 extend_selection: bool,
377 },
378 MoveCursorDown {
380 dom_id: DomId,
381 node_id: NodeId,
382 extend_selection: bool,
383 },
384 MoveCursorToLineStart {
386 dom_id: DomId,
387 node_id: NodeId,
388 extend_selection: bool,
389 },
390 MoveCursorToLineEnd {
392 dom_id: DomId,
393 node_id: NodeId,
394 extend_selection: bool,
395 },
396 MoveCursorToDocumentStart {
398 dom_id: DomId,
399 node_id: NodeId,
400 extend_selection: bool,
401 },
402 MoveCursorToDocumentEnd {
404 dom_id: DomId,
405 node_id: NodeId,
406 extend_selection: bool,
407 },
408
409 AddCursor {
412 dom_id: DomId,
413 node_id: NodeId,
414 cursor: TextCursor,
415 },
416 AddSelectionRange {
418 dom_id: DomId,
419 node_id: NodeId,
420 range: SelectionRange,
421 },
422 RemoveSelectionById {
424 selection_id: azul_core::selection::SelectionId,
425 },
426
427 SetCopyContent {
430 target: DomNodeId,
431 content: ClipboardContent,
432 },
433 SetCutContent {
435 target: DomNodeId,
436 content: ClipboardContent,
437 },
438 SetSelectAllRange {
440 target: DomNodeId,
441 range: SelectionRange,
442 },
443
444 RequestHitTestUpdate { position: LogicalPosition },
451
452 ProcessTextSelectionClick {
461 position: LogicalPosition,
462 time_ms: u64,
463 },
464
465 SetCursorVisibility { visible: bool },
468 ToggleCursorVisibility,
470 ResetCursorBlink,
472 StartCursorBlinkTimer,
474 StopCursorBlinkTimer,
476
477 ScrollActiveCursorIntoView,
481
482 CreateTextInput {
492 text: AzString,
494 },
495
496 BeginInteractiveMove,
501
502 SetDragData {
506 mime_type: AzString,
507 data: Vec<u8>,
508 },
509 AcceptDrop,
512 SetDropEffect {
514 effect: azul_core::drag::DropEffect,
515 },
516
517 InsertChildNode {
523 dom_id: DomId,
524 parent_node_id: NodeId,
525 node_type_str: AzString,
527 position: Option<usize>,
529 classes: Vec<AzString>,
531 id: Option<AzString>,
533 },
534 DeleteNode {
538 dom_id: DomId,
539 node_id: NodeId,
540 },
541 SetNodeIdsAndClasses {
543 dom_id: DomId,
544 node_id: NodeId,
545 ids_and_classes: azul_core::dom::IdOrClassVec,
546 },
547 RemountDom { xml: Option<AzString> },
558
559 SwitchRoute {
566 pattern: AzString,
568 params: azul_core::window::StringPairVec,
570 },
571
572 CommitUndoSnapshot,
575 UndoAppState,
577 RedoAppState,
579}
580
581#[must_use]
600pub fn css_properties_need_relayout(properties: &CssPropertyVec) -> bool {
601 properties
602 .as_ref()
603 .iter()
604 .any(|p| p.get_type().can_trigger_relayout())
605}
606
607pub type CallbackType = extern "C" fn(RefAny, CallbackInfo) -> Update;
609
610#[repr(C)]
614pub struct Callback {
615 pub cb: CallbackType,
616 pub ctx: OptionRefAny,
619}
620
621impl_callback!(Callback, CallbackType);
622
623azul_core::impl_managed_callback! {
630 wrapper: Callback,
631 info_ty: CallbackInfo,
632 return_ty: Update,
633 default_ret: Update::DoNothing,
634 invoker_static: CALLBACK_INVOKER,
635 invoker_ty: AzCallbackInvoker,
636 thunk_fn: az_callback_thunk,
637 setter_fn: AzApp_setCallbackInvoker,
638 from_handle_fn: AzCallback_createFromHostHandle,
639}
640
641impl Callback {
642 #[must_use]
649 pub fn from_ptr(cb: CallbackType) -> Self {
650 Self::from(cb)
651 }
652
653 pub fn create<C: Into<Self>>(cb: C) -> Self {
655 cb.into()
656 }
657
658 #[must_use] pub fn from_core(core: CoreCallback) -> Self {
672 debug_assert!(core.cb != 0, "CoreCallback.cb is null");
673 Self {
674 cb: unsafe { core::mem::transmute::<usize, CallbackType>(core.cb) },
675 ctx: core.ctx,
676 }
677 }
678
679 #[must_use] pub fn to_core(self) -> CoreCallback {
683 CoreCallback {
684 cb: self.cb as usize,
685 ctx: self.ctx,
686 }
687 }
688}
689
690impl From<Callback> for CoreCallback {
692 fn from(callback: Callback) -> Self {
693 callback.to_core()
694 }
695}
696
697impl Callback {
698 #[must_use] pub fn invoke(&self, data: RefAny, info: CallbackInfo) -> Update {
702 (self.cb)(data, info)
703 }
704}
705#[allow(variant_size_differences)] #[derive(Debug, Eq, Clone, PartialEq, PartialOrd, Ord, Hash)]
711#[repr(C, u8)]
712pub enum OptionCallback {
713 None,
715 Some(Callback),
717}
718
719impl OptionCallback {
720 #[must_use] pub fn into_option(self) -> Option<Callback> {
722 match self {
723 Self::None => None,
724 Self::Some(c) => Some(c),
725 }
726 }
727
728 #[must_use] pub const fn is_some(&self) -> bool {
730 matches!(self, Self::Some(_))
731 }
732
733 #[must_use] pub const fn is_none(&self) -> bool {
735 matches!(self, Self::None)
736 }
737}
738
739impl From<Option<Callback>> for OptionCallback {
740 fn from(o: Option<Callback>) -> Self {
741 o.map_or_else(|| Self::None, Self::Some)
742 }
743}
744
745impl From<OptionCallback> for Option<Callback> {
746 fn from(o: OptionCallback) -> Self {
747 o.into_option()
748 }
749}
750
751#[derive(Debug)]
760pub struct CallbackInfoRefData<'a> {
761 pub layout_window: &'a LayoutWindow,
763 pub renderer_resources: &'a RendererResources,
765 pub previous_window_state: &'a Option<FullWindowState>,
767 pub current_window_state: &'a FullWindowState,
769 pub gl_context: &'a OptionGlContextPtr,
771 pub current_scroll_manager: &'a BTreeMap<DomId, BTreeMap<NodeHierarchyItemId, ScrollPosition>>,
773 pub current_window_handle: &'a RawWindowHandle,
775 pub system_callbacks: &'a ExternalSystemCallbacks,
777 pub system_style: Arc<SystemStyle>,
780 pub monitors: Arc<Mutex<MonitorVec>>,
784 #[cfg(feature = "icu")]
787 pub icu_localizer: IcuLocalizerHandle,
788 pub ctx: OptionRefAny,
791}
792
793#[derive(Debug, Clone, Copy)]
816#[repr(C)]
817pub struct CallbackInfo {
818 ref_data: *const CallbackInfoRefData<'static>,
822 hit_dom_node: DomNodeId,
825 cursor_relative_to_item: OptionLogicalPosition,
828 cursor_in_viewport: OptionLogicalPosition,
830 #[cfg(feature = "std")]
834 changes: *const Arc<Mutex<Vec<CallbackChange>>>,
835 #[cfg(not(feature = "std"))]
836 changes: *mut Vec<CallbackChange>,
837}
838
839impl CallbackInfo {
840 #[cfg(feature = "std")]
841 pub const fn new<'a>(
842 ref_data: &'a CallbackInfoRefData<'a>,
843 changes: &'a Arc<Mutex<Vec<CallbackChange>>>,
844 hit_dom_node: DomNodeId,
845 cursor_relative_to_item: OptionLogicalPosition,
846 cursor_in_viewport: OptionLogicalPosition,
847 ) -> Self {
848 Self {
849 ref_data: std::ptr::from_ref::<CallbackInfoRefData<'a>>(ref_data).cast::<CallbackInfoRefData<'static>>(),
855
856 hit_dom_node,
858 cursor_relative_to_item,
859 cursor_in_viewport,
860
861 changes: std::ptr::from_ref::<Arc<Mutex<Vec<CallbackChange>>>>(changes),
863 }
864 }
865
866 #[cfg(not(feature = "std"))]
867 pub fn new<'a>(
868 ref_data: &'a CallbackInfoRefData<'a>,
869 changes: &'a mut Vec<CallbackChange>,
870 hit_dom_node: DomNodeId,
871 cursor_relative_to_item: OptionLogicalPosition,
872 cursor_in_viewport: OptionLogicalPosition,
873 ) -> Self {
874 Self {
875 ref_data: ref_data as *const CallbackInfoRefData<'a> as *const CallbackInfoRefData<'static>,
877 hit_dom_node,
878 cursor_relative_to_item,
879 cursor_in_viewport,
880 changes: changes as *mut Vec<CallbackChange>,
881 }
882 }
883
884 #[must_use] pub fn get_ctx(&self) -> OptionRefAny {
889 unsafe { (*self.ref_data).ctx.clone() }
890 }
891
892 #[must_use] pub fn get_gl_context(&self) -> OptionGlContextPtr {
894 unsafe { (*self.ref_data).gl_context.clone() }
895 }
896
897 #[cfg(feature = "std")]
902 pub fn push_change(&mut self, change: CallbackChange) {
903 unsafe {
905 if let Ok(mut changes) = (*self.changes).lock() {
906 changes.push(change);
907 }
908 }
909 }
910
911 #[cfg(not(feature = "std"))]
912 pub fn push_change(&mut self, change: CallbackChange) {
913 unsafe { (*self.changes).push(change) }
914 }
915
916 pub fn commit_undo_snapshot(&mut self) {
918 self.push_change(CallbackChange::CommitUndoSnapshot);
919 }
920
921 pub fn undo_app_state(&mut self) {
923 self.push_change(CallbackChange::UndoAppState);
924 }
925
926 pub fn redo_app_state(&mut self) {
928 self.push_change(CallbackChange::RedoAppState);
929 }
930
931 #[cfg(feature = "std")]
933 #[must_use] pub const fn get_changes_ptr(&self) -> *const () {
934 self.changes.cast::<()>()
935 }
936
937 #[cfg(feature = "std")]
939 #[must_use] pub fn take_changes(&self) -> Vec<CallbackChange> {
940 unsafe {
942 (*self.changes).lock().map_or_else(
943 |_| Vec::new(),
944 |mut changes| core::mem::take(&mut *changes),
945 )
946 }
947 }
948
949 #[cfg(not(feature = "std"))]
950 pub fn take_changes(&self) -> Vec<CallbackChange> {
951 unsafe { core::mem::take(&mut *self.changes) }
952 }
953
954 #[cfg(feature = "std")]
962 #[must_use] pub fn has_pending_relayout_change(&self) -> bool {
963 unsafe {
964 (*self.changes).lock().is_ok_and(|changes| changes.iter().any(|c| matches!(c,
965 CallbackChange::ModifyWindowState { .. } |
966 CallbackChange::ScrollTo { .. } |
967 CallbackChange::QueueWindowStateSequence { .. }
972 )))
973 }
974 }
975
976 pub fn add_timer(&mut self, timer_id: TimerId, timer: Timer) {
980 self.push_change(CallbackChange::AddTimer { timer_id, timer });
981 }
982
983 pub fn remove_timer(&mut self, timer_id: TimerId) {
985 self.push_change(CallbackChange::RemoveTimer { timer_id });
986 }
987
988 pub fn add_thread(&mut self, thread_id: ThreadId, thread: Thread) {
990 self.push_change(CallbackChange::AddThread { thread_id, thread });
991 }
992
993 pub fn remove_thread(&mut self, thread_id: ThreadId) {
995 self.push_change(CallbackChange::RemoveThread { thread_id });
996 }
997
998 pub fn stop_propagation(&mut self) {
1003 self.push_change(CallbackChange::StopPropagation);
1004 }
1005
1006 pub fn stop_immediate_propagation(&mut self) {
1011 self.push_change(CallbackChange::StopImmediatePropagation);
1012 }
1013
1014 pub fn set_focus(&mut self, target: FocusTarget) {
1016 self.push_change(CallbackChange::SetFocusTarget { target });
1017 }
1018
1019 pub fn create_window(&mut self, options: WindowCreateOptions) {
1021 self.push_change(CallbackChange::CreateNewWindow { options });
1022 }
1023
1024 pub fn close_window(&mut self) {
1026 self.push_change(CallbackChange::CloseWindow);
1027 }
1028
1029 pub fn switch_route(&mut self, pattern: AzString, params: azul_core::window::StringPairVec) {
1040 self.push_change(CallbackChange::SwitchRoute { pattern, params });
1041 }
1042
1043 #[must_use] pub fn get_route_pattern(&self) -> AzString {
1052 match &self.get_current_window_state().active_route {
1053 azul_core::resources::OptionRouteMatch::Some(rm) => rm.pattern.clone(),
1054 azul_core::resources::OptionRouteMatch::None => AzString::from_const_str(""),
1055 }
1056 }
1057
1058 #[allow(clippy::needless_pass_by_value)]
1068 #[must_use] pub fn get_route_param(&self, key: AzString) -> AzString {
1069 match &self.get_current_window_state().active_route {
1070 azul_core::resources::OptionRouteMatch::Some(rm) => {
1071 rm.get_param(key.as_str())
1072 .cloned()
1073 .unwrap_or_else(|| AzString::from_const_str(""))
1074 }
1075 azul_core::resources::OptionRouteMatch::None => AzString::from_const_str(""),
1076 }
1077 }
1078
1079 pub fn set_route_param(&mut self, key: AzString, value: AzString) {
1089 let ws = self.get_current_window_state();
1090 let pattern = match &ws.active_route {
1091 azul_core::resources::OptionRouteMatch::Some(rm) => rm.pattern.clone(),
1092 azul_core::resources::OptionRouteMatch::None => return,
1093 };
1094 let mut params = match &ws.active_route {
1095 azul_core::resources::OptionRouteMatch::Some(rm) => {
1096 rm.params.as_ref().to_vec()
1097 }
1098 azul_core::resources::OptionRouteMatch::None => return,
1099 };
1100 if let Some(existing) = params.iter_mut().find(|p| p.key.as_str() == key.as_str()) {
1102 existing.value = value;
1103 } else {
1104 params.push(azul_core::window::AzStringPair { key, value });
1105 }
1106 self.push_change(CallbackChange::SwitchRoute {
1107 pattern,
1108 params: azul_core::window::StringPairVec::from_vec(params),
1109 });
1110 }
1111
1112 pub fn modify_window_state(&mut self, state: FullWindowState) {
1114 self.push_change(CallbackChange::ModifyWindowState { state });
1115 }
1116
1117 pub fn begin_interactive_move(&mut self) {
1123 self.push_change(CallbackChange::BeginInteractiveMove);
1124 }
1125
1126 pub fn queue_window_state_sequence(&mut self, states: FullWindowStateVec) {
1130 self.push_change(CallbackChange::QueueWindowStateSequence {
1131 states: states.into_library_owned_vec(),
1132 });
1133 }
1134
1135 pub fn change_node_text(&mut self, node_id: DomNodeId, text: AzString) {
1143 self.push_change(CallbackChange::ChangeNodeText { node_id, text });
1144 }
1145
1146 pub fn change_node_image(
1148 &mut self,
1149 dom_id: DomId,
1150 node_id: NodeId,
1151 image: ImageRef,
1152 update_type: UpdateImageType,
1153 ) {
1154 self.push_change(CallbackChange::ChangeNodeImage {
1155 dom_id,
1156 node_id,
1157 image,
1158 update_type,
1159 });
1160 }
1161
1162 pub fn update_image_callback(&mut self, dom_id: DomId, node_id: NodeId) {
1170 self.push_change(CallbackChange::UpdateImageCallback { dom_id, node_id });
1171 }
1172
1173 pub fn update_all_image_callbacks(&mut self) {
1187 self.push_change(CallbackChange::UpdateAllImageCallbacks);
1188 }
1189
1190 pub fn trigger_virtual_view_rerender(&mut self, dom_id: DomId, node_id: NodeId) {
1201 self.push_change(CallbackChange::UpdateVirtualView { dom_id, node_id });
1202 }
1203
1204 pub fn trigger_all_virtual_view_rerender(&mut self) {
1215 self.push_change(CallbackChange::UpdateAllVirtualViews);
1216 }
1217
1218 #[must_use] pub fn get_node_id_by_id_attribute(&self, dom_id: DomId, id: &str) -> Option<NodeId> {
1224 let layout_window = self.get_layout_window();
1225 let layout_result = layout_window.layout_results.get(&dom_id)?;
1226 let styled_dom = &layout_result.styled_dom;
1227
1228 for (node_idx, node_data) in styled_dom.node_data.as_ref().iter().enumerate() {
1230 if node_data.has_id(id) {
1231 return Some(NodeId::new(node_idx));
1232 }
1233 }
1234
1235 None
1236 }
1237
1238 #[must_use] pub fn get_parent_node(&self, dom_id: DomId, node_id: NodeId) -> Option<NodeId> {
1242 let layout_window = self.get_layout_window();
1243 let layout_result = layout_window.layout_results.get(&dom_id)?;
1244 let node_hierarchy = &layout_result.styled_dom.node_hierarchy;
1245 let node = node_hierarchy.as_ref().get(node_id.index())?;
1246 node.parent_id()
1247 }
1248
1249 #[must_use] pub fn get_next_sibling_node(&self, dom_id: DomId, node_id: NodeId) -> Option<NodeId> {
1253 let layout_window = self.get_layout_window();
1254 let layout_result = layout_window.layout_results.get(&dom_id)?;
1255 let node_hierarchy = &layout_result.styled_dom.node_hierarchy;
1256 let node = node_hierarchy.as_ref().get(node_id.index())?;
1257 node.next_sibling_id()
1258 }
1259
1260 #[must_use] pub fn get_previous_sibling_node(&self, dom_id: DomId, node_id: NodeId) -> Option<NodeId> {
1264 let layout_window = self.get_layout_window();
1265 let layout_result = layout_window.layout_results.get(&dom_id)?;
1266 let node_hierarchy = &layout_result.styled_dom.node_hierarchy;
1267 let node = node_hierarchy.as_ref().get(node_id.index())?;
1268 node.previous_sibling_id()
1269 }
1270
1271 #[must_use] pub fn get_first_child_node(&self, dom_id: DomId, node_id: NodeId) -> Option<NodeId> {
1275 let layout_window = self.get_layout_window();
1276 let layout_result = layout_window.layout_results.get(&dom_id)?;
1277 let node_hierarchy = &layout_result.styled_dom.node_hierarchy;
1278 let node = node_hierarchy.as_ref().get(node_id.index())?;
1279 node.first_child_id(node_id)
1280 }
1281
1282 #[must_use] pub fn get_last_child_node(&self, dom_id: DomId, node_id: NodeId) -> Option<NodeId> {
1286 let layout_window = self.get_layout_window();
1287 let layout_result = layout_window.layout_results.get(&dom_id)?;
1288 let node_hierarchy = &layout_result.styled_dom.node_hierarchy;
1289 let node = node_hierarchy.as_ref().get(node_id.index())?;
1290 node.last_child_id()
1291 }
1292
1293 #[must_use] pub fn get_all_children_nodes(&self, dom_id: DomId, node_id: NodeId) -> NodeHierarchyItemIdVec {
1298 let layout_window = self.get_layout_window();
1299 let Some(layout_result) = layout_window.layout_results.get(&dom_id) else {
1300 return NodeHierarchyItemIdVec::from_const_slice(&[]);
1301 };
1302 let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
1303 let Some(hier_item) = node_hierarchy.get(node_id) else {
1304 return NodeHierarchyItemIdVec::from_const_slice(&[]);
1305 };
1306
1307 let Some(first_child) = hier_item.first_child_id(node_id) else {
1309 return NodeHierarchyItemIdVec::from_const_slice(&[]);
1310 };
1311
1312 let mut children: Vec<NodeHierarchyItemId> = Vec::new();
1314 children.push(NodeHierarchyItemId::from_crate_internal(Some(first_child)));
1315
1316 let mut current = first_child;
1317 while let Some(next_sibling) = node_hierarchy
1318 .get(current)
1319 .and_then(azul_core::styled_dom::NodeHierarchyItem::next_sibling_id)
1320 {
1321 children.push(NodeHierarchyItemId::from_crate_internal(Some(next_sibling)));
1322 current = next_sibling;
1323 }
1324
1325 NodeHierarchyItemIdVec::from(children)
1326 }
1327
1328 #[must_use] pub fn get_children_count(&self, dom_id: DomId, node_id: NodeId) -> usize {
1332 let layout_window = self.get_layout_window();
1333 let Some(layout_result) = layout_window.layout_results.get(&dom_id) else {
1334 return 0;
1335 };
1336 let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
1337 let Some(hier_item) = node_hierarchy.get(node_id) else {
1338 return 0;
1339 };
1340
1341 let Some(first_child) = hier_item.first_child_id(node_id) else {
1343 return 0;
1344 };
1345
1346 let mut count = 1;
1348 let mut current = first_child;
1349 while let Some(next_sibling) = node_hierarchy
1350 .get(current)
1351 .and_then(azul_core::styled_dom::NodeHierarchyItem::next_sibling_id)
1352 {
1353 count += 1;
1354 current = next_sibling;
1355 }
1356
1357 count
1358 }
1359
1360 pub fn change_node_image_mask(&mut self, dom_id: DomId, node_id: NodeId, mask: ImageMask) {
1362 self.push_change(CallbackChange::ChangeNodeImageMask {
1363 dom_id,
1364 node_id,
1365 mask,
1366 });
1367 }
1368
1369 pub fn change_node_css_properties(
1371 &mut self,
1372 dom_id: DomId,
1373 node_id: NodeId,
1374 properties: CssPropertyVec,
1375 ) {
1376 self.push_change(CallbackChange::ChangeNodeCssProperties {
1377 dom_id,
1378 node_id,
1379 properties,
1380 });
1381 }
1382
1383 pub fn set_css_property(&mut self, node_id: DomNodeId, property: CssProperty) {
1395 let dom_id = node_id.dom;
1396 let internal_node_id = node_id
1397 .node
1398 .into_crate_internal()
1399 .expect("DomNodeId node should not be None");
1400 self.change_node_css_properties(dom_id, internal_node_id, vec![property].into());
1401 }
1402
1403 pub fn override_node_css_properties(
1410 &mut self,
1411 dom_id: DomId,
1412 node_id: NodeId,
1413 properties: CssPropertyVec,
1414 ) {
1415 self.push_change(CallbackChange::OverrideNodeCssProperties {
1416 dom_id,
1417 node_id,
1418 properties,
1419 });
1420 }
1421
1422 pub fn override_css_property(&mut self, node_id: DomNodeId, property: CssProperty) {
1428 let dom_id = node_id.dom;
1429 let internal_node_id = node_id
1430 .node
1431 .into_crate_internal()
1432 .expect("DomNodeId node should not be None");
1433 self.override_node_css_properties(dom_id, internal_node_id, vec![property].into());
1434 }
1435
1436 pub fn scroll_to(
1438 &mut self,
1439 dom_id: DomId,
1440 node_id: NodeHierarchyItemId,
1441 position: LogicalPosition,
1442 ) {
1443 self.push_change(CallbackChange::ScrollTo {
1444 dom_id,
1445 node_id,
1446 position,
1447 unclamped: false,
1448 });
1449 }
1450
1451 pub fn scroll_to_unclamped(
1454 &mut self,
1455 dom_id: DomId,
1456 node_id: NodeHierarchyItemId,
1457 position: LogicalPosition,
1458 ) {
1459 self.push_change(CallbackChange::ScrollTo {
1460 dom_id,
1461 node_id,
1462 position,
1463 unclamped: true,
1464 });
1465 }
1466
1467 pub fn scroll_node_into_view(
1483 &mut self,
1484 node_id: DomNodeId,
1485 options: crate::managers::scroll_into_view::ScrollIntoViewOptions,
1486 ) {
1487 self.push_change(CallbackChange::ScrollIntoView {
1488 node_id,
1489 options,
1490 });
1491 }
1492
1493 pub fn add_image_to_cache(&mut self, id: AzString, image: ImageRef) {
1495 self.push_change(CallbackChange::AddImageToCache { id, image });
1496 }
1497
1498 pub fn remove_image_from_cache(&mut self, id: AzString) {
1500 self.push_change(CallbackChange::RemoveImageFromCache { id });
1501 }
1502
1503 pub fn reload_system_fonts(&mut self) {
1507 self.push_change(CallbackChange::ReloadSystemFonts);
1508 }
1509
1510 #[must_use] pub const fn get_text_changeset(&self) -> Option<&PendingTextEdit> {
1520 self.get_layout_window()
1521 .text_input_manager
1522 .get_pending_changeset()
1523 }
1524
1525 pub fn set_text_changeset(&mut self, changeset: PendingTextEdit) {
1533 self.push_change(CallbackChange::SetTextChangeset { changeset });
1534 }
1535
1536 pub fn create_text_input(&mut self, text: AzString) {
1552 self.push_change(CallbackChange::CreateTextInput { text });
1553 }
1554
1555 pub fn insert_child_node(
1572 &mut self,
1573 dom_id: DomId,
1574 parent_node_id: NodeId,
1575 node_type_str: AzString,
1576 position: OptionUsize,
1577 classes: StringVec,
1578 id: OptionString,
1579 ) {
1580 self.push_change(CallbackChange::InsertChildNode {
1581 dom_id,
1582 parent_node_id,
1583 node_type_str,
1584 position: position.into(),
1585 classes: classes.into_library_owned_vec(),
1586 id: id.into(),
1587 });
1588 }
1589
1590 pub fn delete_node(&mut self, dom_id: DomId, node_id: NodeId) {
1600 self.push_change(CallbackChange::DeleteNode { dom_id, node_id });
1601 }
1602
1603 pub fn set_node_ids_and_classes(
1612 &mut self,
1613 dom_id: DomId,
1614 node_id: NodeId,
1615 ids_and_classes: azul_core::dom::IdOrClassVec,
1616 ) {
1617 self.push_change(CallbackChange::SetNodeIdsAndClasses {
1618 dom_id,
1619 node_id,
1620 ids_and_classes,
1621 });
1622 }
1623
1624 pub fn prevent_default(&mut self) {
1629 self.push_change(CallbackChange::PreventDefault);
1630 }
1631
1632 pub fn set_cursor_visibility(&mut self, visible: bool) {
1639 self.push_change(CallbackChange::SetCursorVisibility { visible });
1640 }
1641
1642 pub fn reset_cursor_blink(&mut self) {
1648 self.push_change(CallbackChange::ResetCursorBlink);
1649 }
1650
1651 pub fn start_cursor_blink_timer(&mut self) {
1656 self.push_change(CallbackChange::StartCursorBlinkTimer);
1657 }
1658
1659 pub fn stop_cursor_blink_timer(&mut self) {
1663 self.push_change(CallbackChange::StopCursorBlinkTimer);
1664 }
1665
1666 pub fn scroll_active_cursor_into_view(&mut self) {
1671 self.push_change(CallbackChange::ScrollActiveCursorIntoView);
1672 }
1673
1674 pub fn open_menu(&mut self, menu: Menu) {
1683 self.push_change(CallbackChange::OpenMenu {
1684 menu,
1685 position: None,
1686 });
1687 }
1688
1689 pub fn open_menu_at(&mut self, menu: Menu, position: LogicalPosition) {
1695 self.push_change(CallbackChange::OpenMenu {
1696 menu,
1697 position: Some(position),
1698 });
1699 }
1700
1701 pub fn show_tooltip(&mut self, text: AzString) {
1717 let position = self
1718 .get_cursor_relative_to_viewport()
1719 .into_option()
1720 .unwrap_or_else(LogicalPosition::zero);
1721 self.push_change(CallbackChange::ShowTooltip { text, position });
1722 }
1723
1724 pub fn show_tooltip_at(&mut self, text: AzString, position: LogicalPosition) {
1730 self.push_change(CallbackChange::ShowTooltip { text, position });
1731 }
1732
1733 pub fn hide_tooltip(&mut self) {
1735 self.push_change(CallbackChange::HideTooltip);
1736 }
1737
1738 pub fn insert_text(&mut self, dom_id: DomId, node_id: NodeId, text: AzString) {
1750 self.push_change(CallbackChange::InsertText {
1751 dom_id,
1752 node_id,
1753 text,
1754 });
1755 }
1756
1757 pub fn move_cursor(&mut self, dom_id: DomId, node_id: NodeId, cursor: TextCursor) {
1764 self.push_change(CallbackChange::MoveCursor {
1765 dom_id,
1766 node_id,
1767 cursor,
1768 });
1769 }
1770
1771 pub fn set_selection(&mut self, dom_id: DomId, node_id: NodeId, selection: Selection) {
1778 self.push_change(CallbackChange::SetSelection {
1779 dom_id,
1780 node_id,
1781 selection,
1782 });
1783 }
1784
1785 pub fn add_cursor(&mut self, dom_id: DomId, node_id: NodeId, cursor: TextCursor) -> azul_core::selection::SelectionId {
1794 let id = azul_core::selection::SelectionId::new();
1795 self.push_change(CallbackChange::AddCursor {
1796 dom_id,
1797 node_id,
1798 cursor,
1799 });
1800 id
1801 }
1802
1803 pub fn add_selection_range(&mut self, dom_id: DomId, node_id: NodeId, range: SelectionRange) -> azul_core::selection::SelectionId {
1807 let id = azul_core::selection::SelectionId::new();
1808 self.push_change(CallbackChange::AddSelectionRange {
1809 dom_id,
1810 node_id,
1811 range,
1812 });
1813 id
1814 }
1815
1816 pub fn remove_selection_by_id(&mut self, selection_id: azul_core::selection::SelectionId) -> bool {
1820 self.push_change(CallbackChange::RemoveSelectionById {
1821 selection_id,
1822 });
1823 true }
1825
1826 #[must_use] pub fn get_multi_cursor_selections(&self, dom_id: &DomId) -> azul_core::selection::IdentifiedSelectionVec {
1831 let lw = self.get_layout_window();
1832 lw.text_edit_manager.multi_cursor.as_ref()
1833 .map(|mc| mc.selections.clone())
1834 .unwrap_or_default()
1835 .into()
1836 }
1837
1838 #[must_use] pub fn get_primary_selection(&self, dom_id: &DomId) -> Option<azul_core::selection::IdentifiedSelection> {
1840 let lw = self.get_layout_window();
1841 lw.text_edit_manager.multi_cursor.as_ref()
1842 .and_then(|mc| mc.get_primary().copied())
1843 }
1844
1845 #[must_use] pub fn get_selection_count(&self, dom_id: &DomId) -> usize {
1847 let lw = self.get_layout_window();
1848 lw.text_edit_manager.multi_cursor.as_ref()
1849 .map_or(0, azul_core::selection::MultiCursorState::len)
1850 }
1851
1852 pub fn open_menu_for_node(&mut self, menu: Menu, node_id: DomNodeId) -> bool {
1865 let rect = self
1870 .get_node_hit_test_bounds(node_id)
1871 .or_else(|| self.get_node_rect(node_id));
1872 rect.is_some_and(|rect| {
1873 let position = LogicalPosition::new(rect.origin.x, rect.origin.y + rect.size.height);
1875 self.push_change(CallbackChange::OpenMenu {
1876 menu,
1877 position: Some(position),
1878 });
1879 true
1880 })
1881 }
1882
1883 pub fn open_menu_for_hit_node(&mut self, menu: Menu) -> bool {
1895 let hit_node = self.get_hit_node();
1896 self.open_menu_for_node(menu, hit_node)
1897 }
1898
1899 #[must_use] pub const fn get_layout_window(&self) -> &LayoutWindow {
1906 unsafe { (*self.ref_data).layout_window }
1907 }
1908
1909 fn get_inline_layout_for_node(&self, node_id: &DomNodeId) -> Option<&Arc<UnifiedLayout>> {
1920 let layout_window = self.get_layout_window();
1921
1922 let layout_result = layout_window.layout_results.get(&node_id.dom)?;
1924
1925 let dom_node_id = node_id.node.into_crate_internal()?;
1927
1928 let layout_indices = layout_result.layout_tree.dom_to_layout.get(&dom_node_id)?;
1930
1931 let layout_index = *layout_indices.first()?;
1934
1935 let warm_node = layout_result.layout_tree.warm(layout_index)?;
1937 warm_node
1938 .inline_layout_result
1939 .as_ref()
1940 .map(super::solver3::layout_tree::CachedInlineLayout::get_layout)
1941 }
1942
1943 #[must_use] pub fn get_node_size(&self, node_id: DomNodeId) -> Option<LogicalSize> {
1948 self.get_layout_window().get_node_size(node_id)
1949 }
1950
1951 #[must_use] pub fn get_node_position(&self, node_id: DomNodeId) -> Option<LogicalPosition> {
1953 self.get_layout_window().get_node_position(node_id)
1954 }
1955
1956 #[must_use] pub fn get_node_hit_test_bounds(&self, node_id: DomNodeId) -> Option<LogicalRect> {
1961 self.get_layout_window().get_node_hit_test_bounds(node_id)
1962 }
1963
1964 #[must_use] pub fn get_node_rect(&self, node_id: DomNodeId) -> Option<LogicalRect> {
1969 let position = self.get_node_position(node_id)?;
1970 let size = self.get_node_size(node_id)?;
1971 Some(LogicalRect::new(position, size))
1972 }
1973
1974 #[must_use] pub fn get_hit_node_rect(&self) -> Option<LogicalRect> {
1979 let hit_node = self.get_hit_node();
1980 self.get_node_rect(hit_node)
1981 }
1982
1983 #[must_use] pub fn get_timer(&self, timer_id: &TimerId) -> Option<&Timer> {
1987 self.get_layout_window().get_timer(timer_id)
1988 }
1989
1990 #[must_use] pub fn get_timer_ids(&self) -> TimerIdVec {
1992 self.get_layout_window().get_timer_ids()
1993 }
1994
1995 #[must_use] pub fn get_thread(&self, thread_id: &ThreadId) -> Option<&Thread> {
1999 self.get_layout_window().get_thread(thread_id)
2000 }
2001
2002 #[must_use] pub fn get_thread_ids(&self) -> ThreadIdVec {
2004 self.get_layout_window().get_thread_ids()
2005 }
2006
2007 #[must_use] pub fn get_gpu_cache(&self, dom_id: &DomId) -> Option<&GpuValueCache> {
2011 self.get_layout_window().get_gpu_cache(dom_id)
2012 }
2013
2014 #[must_use] pub fn get_layout_result(&self, dom_id: &DomId) -> Option<&DomLayoutResult> {
2018 self.get_layout_window().get_layout_result(dom_id)
2019 }
2020
2021 #[must_use] pub fn get_dom_ids(&self) -> DomIdVec {
2023 self.get_layout_window().get_dom_ids()
2024 }
2025
2026 #[must_use] pub const fn get_hit_node(&self) -> DomNodeId {
2030 self.hit_dom_node
2031 }
2032
2033 #[allow(clippy::trivially_copy_pass_by_ref)] fn is_node_anonymous(&self, dom_id: &DomId, node_id: NodeId) -> bool {
2036 let layout_window = self.get_layout_window();
2037 let Some(layout_result) = layout_window.get_layout_result(dom_id) else {
2038 return false;
2039 };
2040 let node_data_cont = layout_result.styled_dom.node_data.as_container();
2041 let Some(node_data) = node_data_cont.get(node_id) else {
2042 return false;
2043 };
2044 node_data.is_anonymous()
2045 }
2046
2047 #[must_use] pub fn get_parent(&self, node_id: DomNodeId) -> Option<DomNodeId> {
2049 let layout_window = self.get_layout_window();
2050 let layout_result = layout_window.get_layout_result(&node_id.dom)?;
2051 let node_id_internal = node_id.node.into_crate_internal()?;
2052 let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
2053 let hier_item = node_hierarchy.get(node_id_internal)?;
2054
2055 let mut current_parent_id = hier_item.parent_id()?;
2057 loop {
2058 if !self.is_node_anonymous(&node_id.dom, current_parent_id) {
2059 return Some(DomNodeId {
2060 dom: node_id.dom,
2061 node: NodeHierarchyItemId::from_crate_internal(Some(current_parent_id)),
2062 });
2063 }
2064
2065 let parent_hier_item = node_hierarchy.get(current_parent_id)?;
2067 current_parent_id = parent_hier_item.parent_id()?;
2068 }
2069 }
2070
2071 #[must_use] pub fn get_previous_sibling(&self, node_id: DomNodeId) -> Option<DomNodeId> {
2073 let layout_window = self.get_layout_window();
2074 let layout_result = layout_window.get_layout_result(&node_id.dom)?;
2075 let node_id_internal = node_id.node.into_crate_internal()?;
2076 let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
2077 let hier_item = node_hierarchy.get(node_id_internal)?;
2078
2079 let mut current_sibling_id = hier_item.previous_sibling_id()?;
2081 loop {
2082 if !self.is_node_anonymous(&node_id.dom, current_sibling_id) {
2083 return Some(DomNodeId {
2084 dom: node_id.dom,
2085 node: NodeHierarchyItemId::from_crate_internal(Some(current_sibling_id)),
2086 });
2087 }
2088
2089 let sibling_hier_item = node_hierarchy.get(current_sibling_id)?;
2091 current_sibling_id = sibling_hier_item.previous_sibling_id()?;
2092 }
2093 }
2094
2095 #[must_use] pub fn get_next_sibling(&self, node_id: DomNodeId) -> Option<DomNodeId> {
2097 let layout_window = self.get_layout_window();
2098 let layout_result = layout_window.get_layout_result(&node_id.dom)?;
2099 let node_id_internal = node_id.node.into_crate_internal()?;
2100 let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
2101 let hier_item = node_hierarchy.get(node_id_internal)?;
2102
2103 let mut current_sibling_id = hier_item.next_sibling_id()?;
2105 loop {
2106 if !self.is_node_anonymous(&node_id.dom, current_sibling_id) {
2107 return Some(DomNodeId {
2108 dom: node_id.dom,
2109 node: NodeHierarchyItemId::from_crate_internal(Some(current_sibling_id)),
2110 });
2111 }
2112
2113 let sibling_hier_item = node_hierarchy.get(current_sibling_id)?;
2115 current_sibling_id = sibling_hier_item.next_sibling_id()?;
2116 }
2117 }
2118
2119 #[must_use] pub fn get_first_child(&self, node_id: DomNodeId) -> Option<DomNodeId> {
2121 let layout_window = self.get_layout_window();
2122 let layout_result = layout_window.get_layout_result(&node_id.dom)?;
2123 let node_id_internal = node_id.node.into_crate_internal()?;
2124 let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
2125 let hier_item = node_hierarchy.get(node_id_internal)?;
2126
2127 let mut current_child_id = hier_item.first_child_id(node_id_internal)?;
2129 loop {
2130 if !self.is_node_anonymous(&node_id.dom, current_child_id) {
2131 return Some(DomNodeId {
2132 dom: node_id.dom,
2133 node: NodeHierarchyItemId::from_crate_internal(Some(current_child_id)),
2134 });
2135 }
2136
2137 let child_hier_item = node_hierarchy.get(current_child_id)?;
2139 current_child_id = child_hier_item.next_sibling_id()?;
2140 }
2141 }
2142
2143 #[must_use] pub fn get_last_child(&self, node_id: DomNodeId) -> Option<DomNodeId> {
2145 let layout_window = self.get_layout_window();
2146 let layout_result = layout_window.get_layout_result(&node_id.dom)?;
2147 let node_id_internal = node_id.node.into_crate_internal()?;
2148 let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
2149 let hier_item = node_hierarchy.get(node_id_internal)?;
2150
2151 let mut current_child_id = hier_item.last_child_id()?;
2153 loop {
2154 if !self.is_node_anonymous(&node_id.dom, current_child_id) {
2155 return Some(DomNodeId {
2156 dom: node_id.dom,
2157 node: NodeHierarchyItemId::from_crate_internal(Some(current_child_id)),
2158 });
2159 }
2160
2161 let child_hier_item = node_hierarchy.get(current_child_id)?;
2163 current_child_id = child_hier_item.previous_sibling_id()?;
2164 }
2165 }
2166
2167 pub fn get_dataset(&mut self, node_id: DomNodeId) -> Option<RefAny> {
2171 let layout_window = self.get_layout_window();
2172 let layout_result = layout_window.get_layout_result(&node_id.dom)?;
2173 let node_id_internal = node_id.node.into_crate_internal()?;
2174 let node_data_cont = layout_result.styled_dom.node_data.as_container();
2175 let node_data = node_data_cont.get(node_id_internal)?;
2176 node_data.get_dataset().cloned()
2177 }
2178
2179 #[allow(clippy::needless_pass_by_value)]
2182 pub fn get_node_id_of_root_dataset(&mut self, search_key: RefAny) -> Option<DomNodeId> {
2183 let mut found: Option<(u64, DomNodeId)> = None;
2184 let search_type_id = search_key.get_type_id();
2185
2186 for dom_id in self.get_dom_ids().as_ref().iter().copied() {
2187 let layout_window = self.get_layout_window();
2188 let Some(layout_result) = layout_window.get_layout_result(&dom_id) else {
2189 continue;
2190 };
2191
2192 let node_data_cont = layout_result.styled_dom.node_data.as_container();
2193 for (node_idx, node_data) in node_data_cont.iter().enumerate() {
2194 if let Some(dataset) = node_data.get_dataset().cloned() {
2195 if dataset.get_type_id() == search_type_id {
2196 let node_id = DomNodeId {
2197 dom: dom_id,
2198 node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(
2199 node_idx,
2200 ))),
2201 };
2202 let instance_id = dataset.instance_id;
2203
2204 match found {
2205 None => found = Some((instance_id, node_id)),
2206 Some((prev_instance, _)) => {
2207 if instance_id < prev_instance {
2208 found = Some((instance_id, node_id));
2209 }
2210 }
2211 }
2212 }
2213 }
2214 }
2215 }
2216
2217 found.map(|s| s.1)
2218 }
2219
2220 #[must_use] pub fn get_string_contents(&self, node_id: DomNodeId) -> Option<AzString> {
2222 let layout_window = self.get_layout_window();
2223 let layout_result = layout_window.get_layout_result(&node_id.dom)?;
2224 let node_id_internal = node_id.node.into_crate_internal()?;
2225 let node_data_cont = layout_result.styled_dom.node_data.as_container();
2226 let node_data = node_data_cont.get(node_id_internal)?;
2227
2228 if let NodeType::Text(text) = node_data.get_node_type() {
2229 Some(text.clone_self())
2230 } else {
2231 None
2232 }
2233 }
2234
2235 #[must_use] pub fn get_node_tag_name(&self, node_id: DomNodeId) -> Option<AzString> {
2240 let layout_window = self.get_layout_window();
2241 let layout_result = layout_window.get_layout_result(&node_id.dom)?;
2242 let node_id_internal = node_id.node.into_crate_internal()?;
2243 let node_data_cont = layout_result.styled_dom.node_data.as_container();
2244 let node_data = node_data_cont.get(node_id_internal)?;
2245
2246 let tag = node_data.get_node_type().get_path();
2247 Some(tag.to_string().into())
2248 }
2249
2250 #[allow(clippy::match_same_arms)]
2262 #[must_use] pub fn get_node_attribute(&self, node_id: DomNodeId, attr_name: &str) -> Option<AzString> {
2263 use azul_core::dom::AttributeType;
2264
2265 let layout_window = self.get_layout_window();
2266 let layout_result = layout_window.get_layout_result(&node_id.dom)?;
2267 let node_id_internal = node_id.node.into_crate_internal()?;
2268 let node_data_cont = layout_result.styled_dom.node_data.as_container();
2269 let node_data = node_data_cont.get(node_id_internal)?;
2270
2271 for attr in node_data.attributes().as_ref() {
2273 match (attr_name, attr) {
2274 ("id", AttributeType::Id(v)) => return Some(v.clone()),
2275 ("class", AttributeType::Class(v)) => return Some(v.clone()),
2276 ("aria-label", AttributeType::AriaLabel(v)) => return Some(v.clone()),
2277 ("aria-labelledby", AttributeType::AriaLabelledBy(v)) => return Some(v.clone()),
2278 ("aria-describedby", AttributeType::AriaDescribedBy(v)) => return Some(v.clone()),
2279 ("role", AttributeType::AriaRole(v)) => return Some(v.clone()),
2280 ("href", AttributeType::Href(v)) => return Some(v.clone()),
2281 ("rel", AttributeType::Rel(v)) => return Some(v.clone()),
2282 ("target", AttributeType::Target(v)) => return Some(v.clone()),
2283 ("src", AttributeType::Src(v)) => return Some(v.clone()),
2284 ("alt", AttributeType::Alt(v)) => return Some(v.clone()),
2285 ("title", AttributeType::Title(v)) => return Some(v.clone()),
2286 ("name", AttributeType::Name(v)) => return Some(v.clone()),
2287 ("value", AttributeType::Value(v)) => return Some(v.clone()),
2288 ("type", AttributeType::InputType(v)) => return Some(v.clone()),
2289 ("placeholder", AttributeType::Placeholder(v)) => return Some(v.clone()),
2290 ("max", AttributeType::Max(v)) => return Some(v.clone()),
2291 ("min", AttributeType::Min(v)) => return Some(v.clone()),
2292 ("step", AttributeType::Step(v)) => return Some(v.clone()),
2293 ("pattern", AttributeType::Pattern(v)) => return Some(v.clone()),
2294 ("autocomplete", AttributeType::Autocomplete(v)) => return Some(v.clone()),
2295 ("scope", AttributeType::Scope(v)) => return Some(v.clone()),
2296 ("lang", AttributeType::Lang(v)) => return Some(v.clone()),
2297 ("dir", AttributeType::Dir(v)) => return Some(v.clone()),
2298 ("required", AttributeType::Required) => return Some("true".into()),
2299 ("disabled", AttributeType::Disabled) => return Some("true".into()),
2300 ("readonly", AttributeType::Readonly) => return Some("true".into()),
2301 ("checked", AttributeType::CheckedTrue) => return Some("true".into()),
2302 ("checked", AttributeType::CheckedFalse) => return Some("false".into()),
2303 ("selected", AttributeType::Selected) => return Some("true".into()),
2304 ("hidden", AttributeType::Hidden) => return Some("true".into()),
2305 ("focusable", AttributeType::Focusable) => return Some("true".into()),
2306 ("minlength", AttributeType::MinLength(v)) => return Some(v.to_string().into()),
2307 ("maxlength", AttributeType::MaxLength(v)) => return Some(v.to_string().into()),
2308 ("colspan", AttributeType::ColSpan(v)) => return Some(v.to_string().into()),
2309 ("rowspan", AttributeType::RowSpan(v)) => return Some(v.to_string().into()),
2310 ("tabindex", AttributeType::TabIndex(v)) => return Some(v.to_string().into()),
2311 ("contenteditable", AttributeType::ContentEditable(v)) => {
2312 return Some(v.to_string().into())
2313 }
2314 ("draggable", AttributeType::Draggable(v)) => return Some(v.to_string().into()),
2315 (name, AttributeType::Data(nv))
2317 if name.starts_with("data-") && nv.attr_name.as_str() == &name[5..] =>
2318 {
2319 return Some(nv.value.clone());
2320 }
2321 (name, AttributeType::AriaState(nv))
2323 if name == format!("aria-{}", nv.attr_name.as_str()) =>
2324 {
2325 return Some(nv.value.clone());
2326 }
2327 (name, AttributeType::AriaProperty(nv))
2328 if name == format!("aria-{}", nv.attr_name.as_str()) =>
2329 {
2330 return Some(nv.value.clone());
2331 }
2332 (name, AttributeType::Custom(nv)) if nv.attr_name.as_str() == name => {
2334 return Some(nv.value.clone());
2335 }
2336 _ => {}
2337 }
2338 }
2339
2340 None
2341 }
2342
2343 #[must_use] pub fn get_node_classes(&self, node_id: DomNodeId) -> StringVec {
2345 let Some(layout_window) = self.get_layout_window().get_layout_result(&node_id.dom) else {
2346 return StringVec::from_const_slice(&[]);
2347 };
2348 let Some(node_id_internal) = node_id.node.into_crate_internal() else {
2349 return StringVec::from_const_slice(&[]);
2350 };
2351 let node_data_cont = layout_window.styled_dom.node_data.as_container();
2352 let Some(node_data) = node_data_cont.get(node_id_internal) else {
2353 return StringVec::from_const_slice(&[]);
2354 };
2355
2356 let classes: Vec<AzString> = node_data
2357 .attributes()
2358 .as_ref()
2359 .iter()
2360 .filter_map(|attr| {
2361 attr.as_class().map(|c| c.to_string().into())
2362 })
2363 .collect();
2364
2365 StringVec::from(classes)
2366 }
2367
2368 #[must_use] pub fn get_node_id(&self, node_id: DomNodeId) -> Option<AzString> {
2370 let layout_window = self.get_layout_window();
2371 let layout_result = layout_window.get_layout_result(&node_id.dom)?;
2372 let node_id_internal = node_id.node.into_crate_internal()?;
2373 let node_data_cont = layout_result.styled_dom.node_data.as_container();
2374 let node_data = node_data_cont.get(node_id_internal)?;
2375
2376 for attr in node_data.attributes().as_ref() {
2377 if let Some(id) = attr.as_id() {
2378 return Some(id.to_string().into());
2379 }
2380 }
2381 None
2382 }
2383
2384 #[must_use] pub const fn get_selection(&self, _dom_id: &DomId) -> Option<&SelectionState> {
2388 None
2391 }
2392
2393 #[must_use] pub fn has_selection(&self, _dom_id: &DomId) -> bool {
2395 self.get_layout_window()
2396 .text_edit_manager.multi_cursor.as_ref()
2397 .is_some_and(|mc| mc.selections.iter().any(|s| matches!(&s.selection, Selection::Range(_))))
2398 }
2399
2400 #[must_use] pub fn get_primary_cursor(&self, _dom_id: &DomId) -> Option<TextCursor> {
2402 self.get_layout_window()
2403 .text_edit_manager.multi_cursor.as_ref()
2404 .and_then(azul_core::selection::MultiCursorState::get_primary_cursor)
2405 }
2406
2407 #[must_use] pub fn get_selection_ranges(&self, _dom_id: &DomId) -> SelectionRangeVec {
2409 let ranges: Vec<SelectionRange> = self.get_layout_window()
2410 .text_edit_manager.multi_cursor.as_ref()
2411 .map(|mc| mc.selections.iter().filter_map(|s| match &s.selection {
2412 Selection::Range(r) => Some(*r),
2413 Selection::Cursor(_) => None,
2414 }).collect()).unwrap_or_default();
2415 ranges.into()
2416 }
2417
2418 #[must_use] pub const fn get_text_cache(&self) -> &TextLayoutCache {
2431 &self.get_layout_window().text_cache
2432 }
2433
2434 #[must_use] pub const fn get_current_window_state(&self) -> &FullWindowState {
2438 unsafe { (*self.ref_data).current_window_state }
2440 }
2441
2442 #[must_use] pub const fn get_current_window_flags(&self) -> WindowFlags {
2444 self.get_current_window_state().flags
2445 }
2446
2447 #[must_use] pub fn get_current_keyboard_state(&self) -> KeyboardState {
2449 self.get_current_window_state().keyboard_state.clone()
2450 }
2451
2452 #[must_use] pub const fn get_current_mouse_state(&self) -> MouseState {
2454 self.get_current_window_state().mouse_state
2455 }
2456
2457 #[must_use] pub const fn get_previous_window_state(&self) -> &Option<FullWindowState> {
2459 unsafe { (*self.ref_data).previous_window_state }
2460 }
2461
2462 #[must_use] pub fn get_previous_window_flags(&self) -> Option<WindowFlags> {
2464 Some(self.get_previous_window_state().as_ref()?.flags)
2465 }
2466
2467 #[must_use] pub fn get_previous_keyboard_state(&self) -> Option<KeyboardState> {
2469 Some(
2470 self.get_previous_window_state()
2471 .as_ref()?
2472 .keyboard_state
2473 .clone(),
2474 )
2475 }
2476
2477 #[must_use] pub fn get_previous_mouse_state(&self) -> Option<MouseState> {
2479 Some(
2480 self.get_previous_window_state()
2481 .as_ref()?
2482 .mouse_state,
2483 )
2484 }
2485
2486 #[must_use] pub const fn get_cursor_relative_to_node(&self) -> azul_core::geom::OptionCursorNodePosition {
2489 use azul_core::geom::{CursorNodePosition, OptionCursorNodePosition};
2490 match self.cursor_relative_to_item {
2491 OptionLogicalPosition::Some(p) => OptionCursorNodePosition::Some(CursorNodePosition::from_logical(p)),
2492 OptionLogicalPosition::None => OptionCursorNodePosition::None,
2493 }
2494 }
2495
2496 #[must_use] pub const fn get_cursor_relative_to_viewport(&self) -> OptionLogicalPosition {
2497 self.cursor_in_viewport
2498 }
2499
2500 #[allow(clippy::cast_precision_loss)] #[must_use] pub fn get_cursor_position_screen(&self) -> azul_core::geom::OptionScreenPosition {
2523 use azul_core::window::WindowPosition;
2524 use azul_core::geom::{LogicalPosition, ScreenPosition, OptionScreenPosition};
2525
2526 let ws = self.get_current_window_state();
2527 let Some(cursor_local) = ws.mouse_state.cursor_position.get_position() else {
2528 return OptionScreenPosition::None;
2529 };
2530 match ws.position {
2531 WindowPosition::Initialized(pos) => {
2532 OptionScreenPosition::Some(ScreenPosition::new(
2533 pos.x as f32 + cursor_local.x,
2534 pos.y as f32 + cursor_local.y,
2535 ))
2536 }
2537 WindowPosition::Uninitialized | WindowPosition::RelativeToParentWindow(_) => {
2540 OptionScreenPosition::Some(ScreenPosition::new(cursor_local.x, cursor_local.y))
2541 }
2542 }
2543 }
2544
2545 #[must_use] pub fn get_drag_delta(&self) -> azul_core::geom::OptionDragDelta {
2553 use azul_core::geom::{DragDelta, OptionDragDelta};
2554 let gm = self.get_gesture_drag_manager();
2555 match gm.get_drag_delta() {
2556 Some((dx, dy)) => OptionDragDelta::Some(DragDelta::new(dx, dy)),
2557 None => OptionDragDelta::None,
2558 }
2559 }
2560
2561 #[must_use] pub fn get_drag_delta_screen(&self) -> azul_core::geom::OptionDragDelta {
2567 use azul_core::geom::{DragDelta, OptionDragDelta};
2568 let gm = self.get_gesture_drag_manager();
2569 match gm.get_drag_delta_screen() {
2570 Some((dx, dy)) => OptionDragDelta::Some(DragDelta::new(dx, dy)),
2571 None => OptionDragDelta::None,
2572 }
2573 }
2574
2575 #[must_use] pub fn get_drag_delta_screen_incremental(&self) -> azul_core::geom::OptionDragDelta {
2589 use azul_core::geom::{DragDelta, OptionDragDelta};
2590 let gm = self.get_gesture_drag_manager();
2591 match gm.get_drag_delta_screen_incremental() {
2592 Some((dx, dy)) => OptionDragDelta::Some(DragDelta::new(dx, dy)),
2593 None => OptionDragDelta::None,
2594 }
2595 }
2596
2597 #[must_use] pub const fn get_current_window_handle(&self) -> RawWindowHandle {
2598 unsafe { *(*self.ref_data).current_window_handle }
2599 }
2600
2601 #[must_use] pub fn get_system_style(&self) -> Arc<SystemStyle> {
2604 unsafe { (*self.ref_data).system_style.clone() }
2605 }
2606
2607 #[must_use] pub fn get_monitors(&self) -> MonitorVec {
2613 let monitors_arc = unsafe { &(*self.ref_data).monitors };
2614 monitors_arc.lock().map_or_else(|_| MonitorVec::from_const_slice(&[]), |g| g.clone())
2615 }
2616
2617 #[must_use] pub fn get_current_monitor(&self) -> OptionMonitor {
2623 let ws = self.get_current_window_state();
2624 let monitor_index = match ws.monitor_id {
2625 azul_css::corety::OptionU32::Some(idx) => idx as usize,
2626 azul_css::corety::OptionU32::None => return OptionMonitor::None,
2627 };
2628 let monitors_arc = unsafe { &(*self.ref_data).monitors };
2629 let Ok(guard) = monitors_arc.lock() else {
2630 return OptionMonitor::None;
2631 };
2632 for m in guard.as_ref() {
2633 if m.monitor_id.index == monitor_index {
2634 return OptionMonitor::Some(m.clone());
2635 }
2636 }
2637 OptionMonitor::None
2638 }
2639
2640 #[cfg(feature = "icu")]
2655 pub fn get_icu_localizer(&self) -> &IcuLocalizerHandle {
2656 unsafe { &(*self.ref_data).icu_localizer }
2657 }
2658
2659 #[cfg(feature = "icu")]
2672 pub fn format_integer(&self, locale: &str, value: i64) -> AzString {
2673 self.get_icu_localizer().format_integer(locale, value)
2674 }
2675
2676 #[cfg(feature = "icu")]
2689 pub fn format_decimal(&self, locale: &str, integer_part: i64, decimal_places: i16) -> AzString {
2690 self.get_icu_localizer().format_decimal(locale, integer_part, decimal_places)
2691 }
2692
2693 #[cfg(feature = "icu")]
2707 pub fn get_plural_category(&self, locale: &str, value: i64) -> PluralCategory {
2708 self.get_icu_localizer().get_plural_category(locale, value)
2709 }
2710
2711 #[cfg(feature = "icu")]
2724 pub fn pluralize(
2725 &self,
2726 locale: &str,
2727 value: i64,
2728 zero: &str,
2729 one: &str,
2730 two: &str,
2731 few: &str,
2732 many: &str,
2733 other: &str,
2734 ) -> AzString {
2735 self.get_icu_localizer().pluralize(locale, value, zero, one, two, few, many, other)
2736 }
2737
2738 #[cfg(feature = "icu")]
2751 pub fn format_list(&self, locale: &str, items: StringVec, list_type: ListType) -> AzString {
2752 self.get_icu_localizer()
2753 .format_list(locale, items.as_ref(), list_type)
2754 }
2755
2756 #[cfg(feature = "icu")]
2770 pub fn format_date(&self, locale: &str, date: IcuDate, length: FormatLength) -> IcuResult {
2771 self.get_icu_localizer().format_date(locale, date, length)
2772 }
2773
2774 #[cfg(feature = "icu")]
2788 pub fn format_time(&self, locale: &str, time: IcuTime, include_seconds: bool) -> IcuResult {
2789 self.get_icu_localizer().format_time(locale, time, include_seconds)
2790 }
2791
2792 #[cfg(feature = "icu")]
2799 pub fn format_datetime(&self, locale: &str, datetime: IcuDateTime, length: FormatLength) -> IcuResult {
2800 self.get_icu_localizer().format_datetime(locale, datetime, length)
2801 }
2802
2803 #[cfg(feature = "icu")]
2819 pub fn compare_strings(&self, locale: &str, a: &str, b: &str) -> i32 {
2820 self.get_icu_localizer().compare_strings(locale, a, b)
2821 }
2822
2823 #[cfg(feature = "icu")]
2838 pub fn sort_strings(&self, locale: &str, strings: StringVec) -> IcuStringVec {
2839 self.get_icu_localizer()
2840 .sort_strings(locale, strings.as_ref())
2841 }
2842
2843 #[cfg(feature = "icu")]
2853 pub fn strings_equal(&self, locale: &str, a: &str, b: &str) -> bool {
2854 self.get_icu_localizer().strings_equal(locale, a, b)
2855 }
2856
2857 #[must_use] pub fn get_cursor_position(&self) -> Option<LogicalPosition> {
2859 self.cursor_in_viewport.into_option()
2860 }
2861
2862 #[must_use] pub fn get_hit_node_layout_rect(&self) -> Option<LogicalRect> {
2864 self.get_layout_window()
2865 .get_node_layout_rect(self.hit_dom_node)
2866 }
2867
2868 #[must_use] pub fn get_computed_css_property(
2888 &self,
2889 node_id: DomNodeId,
2890 property_type: CssPropertyType,
2891 ) -> Option<CssProperty> {
2892 let layout_window = self.get_layout_window();
2893
2894 let layout_result = layout_window.layout_results.get(&node_id.dom)?;
2896
2897 let styled_dom = &layout_result.styled_dom;
2899
2900 let internal_node_id = node_id.node.into_crate_internal()?;
2902
2903 let node_data_container = styled_dom.node_data.as_container();
2905 let node_data = node_data_container.get(internal_node_id)?;
2906
2907 let styled_nodes_container = styled_dom.styled_nodes.as_container();
2909 let styled_node = styled_nodes_container.get(internal_node_id)?;
2910 let node_state = &styled_node.styled_node_state;
2911
2912 let css_property_cache = &styled_dom.css_property_cache.ptr;
2914 css_property_cache
2915 .get_property(node_data, &internal_node_id, node_state, &property_type)
2916 .cloned()
2917 }
2918
2919 #[must_use] pub fn get_computed_width(&self, node_id: DomNodeId) -> Option<CssProperty> {
2923 self.get_computed_css_property(node_id, CssPropertyType::Width)
2924 }
2925
2926 #[must_use] pub fn get_computed_height(&self, node_id: DomNodeId) -> Option<CssProperty> {
2930 self.get_computed_css_property(node_id, CssPropertyType::Height)
2931 }
2932
2933 #[must_use] pub const fn get_system_time_fn(&self) -> GetSystemTimeCallback {
2936 unsafe { (*self.ref_data).system_callbacks.get_system_time_fn }
2937 }
2938
2939 #[must_use] pub fn get_current_time(&self) -> task::Instant {
2940 let cb = self.get_system_time_fn();
2941 (cb.cb)()
2942 }
2943
2944 #[must_use] pub const fn get_renderer_resources(&self) -> &RendererResources {
2949 unsafe { (*self.ref_data).renderer_resources }
2950 }
2951
2952 #[cfg(feature = "text_layout")]
2986 #[must_use] pub fn get_loaded_fonts(&self) -> LoadedFontVec {
2987 let font_manager = &self.get_layout_window().font_manager;
2988 let Ok(guard) = font_manager.parsed_fonts.lock() else {
2989 return Vec::new().into();
2990 };
2991 let mut out: Vec<LoadedFont> = guard
2994 .values()
2995 .map(|font_ref| {
2996 let parsed = crate::font_ref_to_parsed_font(font_ref);
2997 let family_name = parsed
2998 .font_name
2999 .as_ref()
3000 .map(|s| AzString::from(s.clone()))
3001 .unwrap_or_default();
3002 LoadedFont {
3003 font_hash: parsed.hash,
3004 family_name,
3005 num_glyphs: u32::from(parsed.num_glyphs),
3006 has_bytes: parsed.source_bytes_for_subset().is_some(),
3007 }
3008 })
3009 .collect();
3010 out.sort_by(|a, b| a.font_hash.cmp(&b.font_hash));
3011 out.into()
3012 }
3013
3014 #[cfg(feature = "text_layout")]
3024 #[must_use] pub fn get_loaded_font_bytes(&self, font_hash: u64) -> OptionU8Vec {
3025 let font_manager = &self.get_layout_window().font_manager;
3026 let Some(font_ref) = font_manager.resolve_font_by_hash(font_hash) else {
3030 return OptionU8Vec::None;
3031 };
3032 let parsed = crate::font_ref_to_parsed_font(&font_ref);
3033 parsed.source_bytes_for_subset().map_or_else(|| OptionU8Vec::None, |bytes| OptionU8Vec::Some(U8Vec::from_vec(bytes.as_slice().to_vec())))
3034 }
3035
3036 #[cfg(feature = "cpurender")]
3065 pub fn take_screenshot(&self, dom_id: DomId) -> Result<alloc::vec::Vec<u8>, AzString> {
3069 use crate::cpurender::{render_with_font_manager_and_scroll, CpuRenderState, RenderOptions, ScrollOffsetMap};
3070
3071 let layout_window = self.get_layout_window();
3072 let renderer_resources = &layout_window.renderer_resources;
3073
3074 let layout_result = layout_window
3076 .layout_results
3077 .get(&dom_id)
3078 .ok_or_else(|| AzString::from("DOM not found in layout results"))?;
3079
3080 let ws = self.get_current_window_state();
3082 let width = ws.size.dimensions.width;
3083 let height = ws.size.dimensions.height;
3084
3085 if width <= 0.0 || height <= 0.0 {
3086 return Err(AzString::from("Invalid viewport dimensions"));
3087 }
3088
3089 let display_list = &layout_result.display_list;
3090 let dpi_factor = ws.size.get_hidpi_factor().inner.get();
3091
3092 let scroll_offsets = layout_window.scroll_manager
3094 .build_scroll_offset_map(dom_id, &layout_result.scroll_ids);
3095
3096 let gpu_cache = layout_window.gpu_state_manager
3100 .get_cache(dom_id);
3101 let render_state = CpuRenderState::from_gpu_cache(
3102 gpu_cache,
3103 dom_id,
3104 &scroll_offsets,
3105 )
3106 .with_system_style(layout_window.system_style.clone());
3107
3108 let opts = RenderOptions {
3109 width,
3110 height,
3111 dpi_factor,
3112 };
3113
3114 let mut glyph_cache = crate::glyph_cache::GlyphCache::new();
3115 let pixmap = render_with_font_manager_and_scroll(
3116 display_list,
3117 renderer_resources,
3118 &layout_window.font_manager,
3119 opts,
3120 &mut glyph_cache,
3121 &render_state,
3122 ).map_err(AzString::from)?;
3123
3124 let png_data = pixmap
3126 .encode_png()
3127 .map_err(|e| AzString::from(alloc::format!("PNG encoding failed: {e}")))?;
3128
3129 Ok(png_data)
3130 }
3131
3132 #[cfg(all(feature = "std", feature = "cpurender"))]
3144 pub fn take_screenshot_to_file(&self, dom_id: DomId, path: &str) -> Result<(), AzString> {
3148 let png_data = self.take_screenshot(dom_id)?;
3149 std::fs::write(path, png_data)
3150 .map_err(|e| AzString::from(alloc::format!("Failed to write file: {e}")))?;
3151 Ok(())
3152 }
3153
3154 #[cfg(feature = "std")]
3163 pub fn take_native_screenshot(&self, _path: &str) -> Result<(), AzString> {
3167 Err(AzString::from(
3168 "Native screenshot requires the NativeScreenshotExt trait from azul-dll crate. \
3169 Import it with: use azul::desktop::NativeScreenshotExt;",
3170 ))
3171 }
3172
3173 #[cfg(feature = "std")]
3182 pub fn take_native_screenshot_bytes(&self) -> Result<alloc::vec::Vec<u8>, AzString> {
3186 let temp_path = std::env::temp_dir().join("azul_screenshot_temp.png");
3188 let temp_path_str = temp_path.to_string_lossy().to_string();
3189
3190 self.take_native_screenshot(&temp_path_str)?;
3191
3192 let bytes = std::fs::read(&temp_path)
3193 .map_err(|e| AzString::from(alloc::format!("Failed to read screenshot: {e}")))?;
3194
3195 drop(std::fs::remove_file(&temp_path));
3196
3197 Ok(bytes)
3198 }
3199
3200 #[cfg(feature = "std")]
3210 pub fn take_native_screenshot_base64(&self) -> Result<AzString, AzString> {
3214 let png_bytes = self.take_native_screenshot_bytes()?;
3215 let base64_str = base64_encode(&png_bytes);
3216 Ok(AzString::from(alloc::format!(
3217 "data:image/png;base64,{base64_str}"
3218 )))
3219 }
3220
3221 #[cfg(feature = "cpurender")]
3230 pub fn take_screenshot_base64(&self, dom_id: DomId) -> Result<AzString, AzString> {
3234 let png_bytes = self.take_screenshot(dom_id)?;
3235 let base64_str = base64_encode(&png_bytes);
3236 Ok(AzString::from(alloc::format!(
3237 "data:image/png;base64,{base64_str}"
3238 )))
3239 }
3240
3241 #[must_use] pub const fn get_scroll_manager(&self) -> &ScrollManager {
3248 unsafe { &(*self.ref_data).layout_window.scroll_manager }
3249 }
3250
3251 #[must_use] pub const fn get_gesture_drag_manager(&self) -> &GestureAndDragManager {
3259 unsafe { &(*self.ref_data).layout_window.gesture_drag_manager }
3260 }
3261
3262 pub fn inject_native_gesture(
3270 &mut self,
3271 gesture: crate::managers::gesture::NativeGestureEvent,
3272 ) {
3273 self.push_change(CallbackChange::InjectNativeGesture { gesture });
3274 }
3275
3276 pub fn perform_accessibility_action(
3289 &mut self,
3290 dom_id: DomId,
3291 node_id: NodeId,
3292 action: AccessibilityAction,
3293 ) {
3294 self.push_change(CallbackChange::PerformAccessibilityAction {
3295 dom_id,
3296 node_id,
3297 action,
3298 });
3299 }
3300
3301 #[must_use] pub const fn get_focus_manager(&self) -> &FocusManager {
3306 &self.get_layout_window().focus_manager
3307 }
3308
3309 #[must_use] pub const fn get_undo_redo_manager(&self) -> &UndoRedoManager {
3314 &self.get_layout_window().undo_redo_manager
3315 }
3316
3317 #[must_use] pub const fn get_hover_manager(&self) -> &HoverManager {
3322 &self.get_layout_window().hover_manager
3323 }
3324
3325 #[must_use] pub const fn get_text_input_manager(&self) -> &TextInputManager {
3329 &self.get_layout_window().text_input_manager
3330 }
3331
3332 #[must_use] pub fn has_any_selection(&self) -> bool {
3336 self.get_layout_window()
3337 .text_edit_manager.multi_cursor.as_ref()
3338 .is_some_and(|mc| mc.selections.iter().any(|s| matches!(&s.selection, Selection::Range(_))))
3339 }
3340
3341 #[must_use] pub fn is_node_focused(&self, node_id: DomNodeId) -> bool {
3343 self.get_focus_manager().has_focus(&node_id)
3344 }
3345
3346 #[must_use] pub fn is_dom_focused(&self, dom_id: DomId) -> bool {
3348 self.get_focused_node()
3349 .is_some_and(|n| n.dom == dom_id)
3350 }
3351
3352 #[must_use] pub const fn get_pen_state(&self) -> Option<&PenState> {
3356 self.get_gesture_drag_manager().get_pen_state()
3357 }
3358
3359 #[must_use] pub const fn get_wacom_pad(&self) -> Option<crate::managers::gesture::WacomPadState> {
3365 self.get_gesture_drag_manager().get_pad_state().copied()
3366 }
3367
3368 #[must_use] pub const fn get_location_fix(&self) -> Option<azul_core::geolocation::LocationFix> {
3375 self.get_layout_window().geolocation_manager.latest_fix()
3376 }
3377
3378 #[must_use] pub const fn get_sensor_reading(
3384 &self,
3385 kind: azul_core::sensors::SensorKind,
3386 ) -> Option<azul_core::sensors::SensorReading> {
3387 self.get_layout_window().sensor_manager.reading(kind)
3388 }
3389
3390 #[must_use] pub const fn get_safe_area_insets(&self) -> azul_css::system::SafeAreaInsets {
3396 self.get_layout_window().safe_area_insets
3397 }
3398
3399 #[must_use] pub fn get_gamepad_state(
3407 &self,
3408 id: azul_core::gamepad::GamepadId,
3409 ) -> Option<azul_core::gamepad::GamepadState> {
3410 self.get_layout_window().gamepad_manager.state(id)
3411 }
3412
3413 #[must_use] pub fn get_primary_gamepad(&self) -> Option<azul_core::gamepad::GamepadState> {
3416 self.get_layout_window().gamepad_manager.primary()
3417 }
3418
3419 #[must_use] pub const fn get_biometric_result(&self) -> Option<azul_core::biometric::BiometricResult> {
3426 self.get_layout_window().biometric_manager.last_result()
3427 }
3428
3429 #[must_use] pub const fn get_biometric_kind(&self) -> azul_core::biometric::BiometricKind {
3434 self.get_layout_window().biometric_manager.availability()
3435 }
3436
3437 pub fn request_biometric_auth(&mut self, prompt: azul_core::biometric::BiometricPrompt) {
3448 crate::managers::biometric::push_biometric_request(prompt);
3449 }
3450
3451 pub fn keyring_store(&mut self, key: AzString, secret: AzString, require_biometry: bool) {
3457 crate::managers::keyring::push_keyring_request(
3458 azul_core::keyring::KeyringRequest::Store {
3459 key,
3460 secret,
3461 require_biometry,
3462 },
3463 );
3464 }
3465
3466 pub fn keyring_get(&mut self, key: AzString) {
3470 crate::managers::keyring::push_keyring_request(azul_core::keyring::KeyringRequest::Get {
3471 key,
3472 });
3473 }
3474
3475 pub fn keyring_delete(&mut self, key: AzString) {
3478 crate::managers::keyring::push_keyring_request(
3479 azul_core::keyring::KeyringRequest::Delete { key },
3480 );
3481 }
3482
3483 #[must_use] pub fn get_keyring_result(&self) -> Option<azul_core::keyring::KeyringResult> {
3488 self.get_layout_window().keyring_manager.last_result().cloned()
3489 }
3490
3491 #[must_use] pub fn get_permission_status(
3498 &self,
3499 capability: crate::managers::permission::Capability,
3500 ) -> crate::managers::permission::PermissionState {
3501 self.get_layout_window()
3502 .permission_manager
3503 .get_status(capability)
3504 }
3505
3506 #[must_use] pub fn get_pen_pressure(&self) -> Option<f32> {
3509 self.get_pen_state().map(|pen| pen.pressure)
3510 }
3511
3512 #[must_use] pub fn get_pen_tilt(&self) -> Option<PenTilt> {
3515 self.get_pen_state().map(|pen| pen.tilt)
3516 }
3517
3518 #[must_use] pub fn is_pen_in_contact(&self) -> bool {
3520 self.get_pen_state()
3521 .is_some_and(|pen| pen.in_contact)
3522 }
3523
3524 #[must_use] pub fn is_pen_eraser(&self) -> bool {
3526 self.get_pen_state()
3527 .is_some_and(|pen| pen.is_eraser)
3528 }
3529
3530 #[must_use] pub fn is_pen_barrel_button_pressed(&self) -> bool {
3532 self.get_pen_state()
3533 .is_some_and(|pen| pen.barrel_button_pressed)
3534 }
3535
3536 #[must_use] pub fn get_last_input_sample(&self) -> Option<&InputSample> {
3538 let manager = self.get_gesture_drag_manager();
3539 manager
3540 .get_current_session()
3541 .and_then(|session| session.last_sample())
3542 }
3543
3544 #[must_use] pub fn get_current_event_id(&self) -> Option<u64> {
3546 self.get_last_input_sample().map(|sample| sample.event_id)
3547 }
3548
3549 #[must_use] pub fn get_swipe_direction(&self) -> crate::managers::gesture::OptionGestureDirection {
3564 self.get_gesture_drag_manager().detect_swipe_direction().into()
3565 }
3566
3567 #[must_use] pub fn get_pinch(&self) -> crate::managers::gesture::OptionDetectedPinch {
3569 self.get_gesture_drag_manager().detect_pinch().into()
3570 }
3571
3572 #[must_use] pub fn get_rotation(&self) -> crate::managers::gesture::OptionDetectedRotation {
3574 self.get_gesture_drag_manager().detect_rotation().into()
3575 }
3576
3577 #[must_use] pub fn get_long_press(&self) -> crate::managers::gesture::OptionDetectedLongPress {
3580 self.get_gesture_drag_manager().detect_long_press().into()
3581 }
3582
3583 #[must_use] pub fn was_double_clicked(&self) -> bool {
3586 self.get_gesture_drag_manager().detect_double_click()
3587 }
3588
3589 pub fn set_focus_to_node(&mut self, dom_id: DomId, node_id: NodeId) {
3593 self.set_focus(FocusTarget::Id(DomNodeId {
3594 dom: dom_id,
3595 node: NodeHierarchyItemId::from_crate_internal(Some(node_id)),
3596 }));
3597 }
3598
3599 pub fn set_focus_to_path(&mut self, dom_id: DomId, css_path: CssPath) {
3601 self.set_focus(FocusTarget::Path(FocusTargetPath {
3602 dom: dom_id,
3603 css_path,
3604 }));
3605 }
3606
3607 pub fn focus_next(&mut self) {
3609 self.set_focus(FocusTarget::Next);
3610 }
3611
3612 pub fn focus_previous(&mut self) {
3614 self.set_focus(FocusTarget::Previous);
3615 }
3616
3617 pub fn focus_first(&mut self) {
3619 self.set_focus(FocusTarget::First);
3620 }
3621
3622 pub fn focus_last(&mut self) {
3624 self.set_focus(FocusTarget::Last);
3625 }
3626
3627 pub fn clear_focus(&mut self) {
3629 self.set_focus(FocusTarget::NoFocus);
3630 }
3631
3632 #[must_use] pub const fn is_dragging(&self) -> bool {
3638 self.get_gesture_drag_manager().is_dragging()
3639 }
3640
3641 #[must_use] pub const fn get_focused_node(&self) -> Option<DomNodeId> {
3645 self.get_layout_window()
3646 .focus_manager
3647 .get_focused_node()
3648 .copied()
3649 }
3650
3651 #[must_use] pub fn has_focus(&self, node_id: DomNodeId) -> bool {
3653 self.get_layout_window().focus_manager.has_focus(&node_id)
3654 }
3655
3656 #[must_use] pub fn get_hovered_file(&self) -> Option<&AzString> {
3663 self.get_layout_window()
3664 .file_drop_manager
3665 .get_hovered_file()
3666 }
3667
3668 #[must_use] pub fn get_hovered_files(&self) -> StringVec {
3671 self.get_layout_window()
3672 .file_drop_manager
3673 .get_hovered_files()
3674 .to_vec()
3675 .into()
3676 }
3677
3678 #[must_use] pub fn get_dropped_file(&self) -> Option<&AzString> {
3684 self.get_layout_window()
3685 .file_drop_manager
3686 .get_dropped_file()
3687 }
3688
3689 #[must_use] pub fn get_dropped_files(&self) -> StringVec {
3691 self.get_layout_window()
3692 .file_drop_manager
3693 .get_dropped_files()
3694 .to_vec()
3695 .into()
3696 }
3697
3698 #[cfg(feature = "std")]
3706 #[must_use] pub fn measure_dom(
3707 &self,
3708 dom: azul_core::dom::Dom,
3709 available: LogicalSize,
3710 ) -> LogicalSize {
3711 self.get_layout_window().measure_dom(dom, available)
3712 }
3713
3714 #[must_use] pub fn get_deepest_hovered_node(&self) -> Option<DomNodeId> {
3719 let hit = self
3720 .get_layout_window()
3721 .hover_manager
3722 .get_current(&InputPointId::Mouse)?;
3723 hit.hovered_nodes.iter().next().and_then(|(dom_id, entry)| {
3724 entry.regular_hit_test_nodes.keys().next_back().map(|nid| DomNodeId {
3725 dom: *dom_id,
3726 node: NodeHierarchyItemId::from_crate_internal(Some(*nid)),
3727 })
3728 })
3729 }
3730
3731 #[must_use] pub const fn is_drag_active(&self) -> bool {
3737 self.get_layout_window().gesture_drag_manager.is_dragging()
3738 }
3739
3740 #[must_use] pub fn is_node_drag_active(&self) -> bool {
3742 self.get_layout_window().gesture_drag_manager.is_node_drag_active()
3743 }
3744
3745 #[must_use] pub fn is_file_drag_active(&self) -> bool {
3747 let lw = self.get_layout_window();
3748 lw.gesture_drag_manager.is_file_dropping()
3753 || !lw.file_drop_manager.get_hovered_files().is_empty()
3754 }
3755
3756 #[must_use] pub fn get_drag_state(&self) -> Option<crate::managers::drag_drop::DragState> {
3760 let ctx = self.get_layout_window().gesture_drag_manager.get_drag_context()?;
3761 crate::managers::drag_drop::DragState::from_context(ctx)
3762 }
3763
3764 #[must_use] pub const fn get_drag_context(&self) -> Option<&azul_core::drag::DragContext> {
3769 self.get_layout_window().gesture_drag_manager.get_drag_context()
3774 }
3775
3776 #[must_use] pub fn get_current_hit_test(&self) -> Option<&FullHitTest> {
3780 self.get_hover_manager().get_current(&InputPointId::Mouse)
3781 }
3782
3783 #[must_use] pub fn get_hit_test_frame(&self, frames_ago: usize) -> Option<&FullHitTest> {
3785 self.get_hover_manager()
3786 .get_frame(&InputPointId::Mouse, frames_ago)
3787 }
3788
3789 #[must_use] pub fn get_hit_test_history(&self) -> Option<&VecDeque<FullHitTest>> {
3793 self.get_hover_manager().get_history(&InputPointId::Mouse)
3794 }
3795
3796 #[must_use] pub fn has_sufficient_history_for_gestures(&self) -> bool {
3798 self.get_hover_manager()
3799 .has_sufficient_history_for_gestures(&InputPointId::Mouse)
3800 }
3801
3802 #[must_use] pub const fn get_file_drop_manager(&self) -> &FileDropManager {
3806 &self.get_layout_window().file_drop_manager
3807 }
3808
3809 #[must_use] pub fn get_dragged_node(&self) -> Option<DomNodeId> {
3814 self.get_drag_context()
3815 .and_then(|ctx| {
3816 ctx.as_node_drag().map(|node_drag| {
3817 DomNodeId {
3818 dom: node_drag.dom_id,
3819 node: NodeHierarchyItemId::from_crate_internal(Some(node_drag.node_id)),
3820 }
3821 })
3822 })
3823 }
3824
3825 #[must_use] pub fn get_dragged_file(&self) -> Option<&AzString> {
3827 self.get_drag_context()
3830 .and_then(|ctx| {
3831 ctx.as_file_drop().and_then(|file_drop| {
3832 file_drop.files.as_ref().first()
3833 })
3834 })
3835 .or_else(|| {
3836 let lw = self.get_layout_window();
3837 lw.file_drop_manager
3838 .get_hovered_files()
3839 .first()
3840 .or_else(|| lw.file_drop_manager.get_dropped_files().first())
3841 })
3842 }
3843
3844 #[must_use] pub fn get_drag_types(&self) -> StringVec {
3849 let lw = self.get_layout_window();
3850 if let Some(ctx) = lw.gesture_drag_manager.get_drag_context() {
3852 if let Some(node_drag) = ctx.as_node_drag() {
3853 return node_drag
3854 .drag_data
3855 .data
3856 .as_ref()
3857 .iter()
3858 .map(|e| e.mime_type.clone())
3859 .collect();
3860 }
3861 }
3862 StringVec::from_const_slice(&[])
3863 }
3864
3865 #[must_use] pub fn get_drag_data(&self, mime_type: &str) -> OptionU8Vec {
3870 let lw = self.get_layout_window();
3871 if let Some(ctx) = lw.gesture_drag_manager.get_drag_context() {
3872 if let Some(node_drag) = ctx.as_node_drag() {
3873 return node_drag.drag_data.get_data(mime_type).map(|d| U8Vec::from(d.to_vec())).into();
3874 }
3875 }
3876 OptionU8Vec::None
3877 }
3878
3879 pub fn set_drag_data(&mut self, mime_type: AzString, data: Vec<u8>) {
3884 self.push_change(CallbackChange::SetDragData { mime_type, data });
3885 }
3886
3887 pub fn accept_drop(&mut self) {
3894 self.push_change(CallbackChange::AcceptDrop);
3895 }
3896
3897 pub fn set_drop_effect(&mut self, effect: azul_core::drag::DropEffect) {
3902 self.push_change(CallbackChange::SetDropEffect { effect });
3903 }
3904
3905 #[must_use] pub fn get_scroll_offset(&self) -> Option<LogicalPosition> {
3912 self.get_scroll_offset_for_node(
3913 self.hit_dom_node.dom,
3914 self.hit_dom_node.node.into_crate_internal()?,
3915 )
3916 }
3917
3918 #[must_use] pub fn get_scroll_offset_for_node(
3920 &self,
3921 dom_id: DomId,
3922 node_id: NodeId,
3923 ) -> Option<LogicalPosition> {
3924 self.get_scroll_manager()
3925 .get_current_offset(dom_id, node_id)
3926 }
3927
3928 #[must_use] pub fn get_scroll_state(&self, dom_id: DomId, node_id: NodeId) -> Option<&AnimatedScrollState> {
3930 self.get_scroll_manager().get_scroll_state(dom_id, node_id)
3931 }
3932
3933 #[must_use] pub fn get_scroll_node_info(
3938 &self,
3939 dom_id: DomId,
3940 node_id: NodeId,
3941 ) -> Option<crate::managers::scroll_state::ScrollNodeInfo> {
3942 self.get_scroll_manager()
3943 .get_scroll_node_info(dom_id, node_id)
3944 }
3945
3946 #[must_use] pub const fn get_scroll_delta(
3957 &self,
3958 _dom_id: DomId,
3959 _node_id: NodeId,
3960 ) -> Option<LogicalPosition> {
3961 self.get_scroll_manager().pending_wheel_event
3962 }
3963
3964 #[must_use] pub const fn had_scroll_activity(
3967 &self,
3968 _dom_id: DomId,
3969 _node_id: NodeId,
3970 ) -> bool {
3971 false
3972 }
3973
3974 #[must_use] pub fn find_scroll_parent(
3979 &self,
3980 dom_id: DomId,
3981 node_id: NodeId,
3982 ) -> Option<NodeId> {
3983 let layout_window = self.get_layout_window();
3984 let layout_results = &layout_window.layout_results;
3985 let lr = layout_results.get(&dom_id)?;
3986 let node_hierarchy: &[azul_core::styled_dom::NodeHierarchyItem] =
3987 lr.styled_dom.node_hierarchy.as_ref();
3988 self.get_scroll_manager()
3989 .find_scroll_parent(dom_id, node_id, node_hierarchy)
3990 }
3991
3992 #[cfg(feature = "std")]
3998 #[must_use] pub fn get_scroll_input_queue(
3999 &self,
4000 ) -> crate::managers::scroll_state::ScrollInputQueue {
4001 self.get_scroll_manager().scroll_input_queue.clone()
4002 }
4003
4004 #[must_use] pub const fn get_gpu_state_manager(&self) -> &GpuStateManager {
4008 &self.get_layout_window().gpu_state_manager
4009 }
4010
4011 #[must_use] pub const fn get_virtual_view_manager(&self) -> &VirtualViewManager {
4015 &self.get_layout_window().virtual_view_manager
4016 }
4017
4018 #[must_use] pub fn inspect_copy_changeset(&self, target: DomNodeId) -> Option<ClipboardContent> {
4026 let layout_window = self.get_layout_window();
4027 let dom_id = &target.dom;
4028 layout_window.get_selected_content_for_clipboard(dom_id)
4029 }
4030
4031 #[must_use] pub fn inspect_cut_changeset(&self, target: DomNodeId) -> Option<ClipboardContent> {
4036 self.inspect_copy_changeset(target)
4038 }
4039
4040 #[must_use] pub fn inspect_paste_target_range(&self, _target: DomNodeId) -> Option<SelectionRange> {
4045 let layout_window = self.get_layout_window();
4046 layout_window
4047 .text_edit_manager.multi_cursor.as_ref()
4048 .and_then(|mc| mc.selections.iter().find_map(|s| match &s.selection {
4049 Selection::Range(r) => Some(*r),
4050 Selection::Cursor(_) => None,
4051 }))
4052 }
4053
4054 #[must_use] pub fn inspect_select_all_changeset(&self, target: DomNodeId) -> Option<SelectAllResult> {
4058 use azul_core::selection::{CursorAffinity, GraphemeClusterId, TextCursor};
4059
4060 let layout_window = self.get_layout_window();
4061 let node_id = target.node.into_crate_internal()?;
4062
4063 let content = layout_window.get_text_before_textinput(target.dom, node_id);
4065 let text = layout_window.extract_text_from_inline_content(&content);
4066
4067 let start_cursor = TextCursor {
4069 cluster_id: GraphemeClusterId {
4070 source_run: 0,
4071 start_byte_in_run: 0,
4072 },
4073 affinity: CursorAffinity::Leading,
4074 };
4075
4076 let end_cursor = TextCursor {
4077 cluster_id: GraphemeClusterId {
4078 source_run: 0,
4079 start_byte_in_run: u32::try_from(text.len()).unwrap_or(u32::MAX),
4080 },
4081 affinity: CursorAffinity::Leading,
4082 };
4083
4084 let range = SelectionRange {
4085 start: start_cursor,
4086 end: end_cursor,
4087 };
4088
4089 Some(SelectAllResult {
4090 full_text: text.into(),
4091 selection_range: range,
4092 })
4093 }
4094
4095 #[must_use] pub fn inspect_delete_changeset(
4104 &self,
4105 target: DomNodeId,
4106 forward: bool,
4107 ) -> Option<DeleteResult> {
4108 let layout_window = self.get_layout_window();
4109 let dom_id = &target.dom;
4110 let node_id = target.node.into_crate_internal()?;
4111
4112 let content = layout_window.get_text_before_textinput(target.dom, node_id);
4114
4115 let selection = if let Some(mc) = layout_window.text_edit_manager.multi_cursor.as_ref() {
4117 if let Some(range) = mc.selections.iter().find_map(|s| match &s.selection {
4118 Selection::Range(r) => Some(*r),
4119 Selection::Cursor(_) => None,
4120 }) {
4121 Selection::Range(range)
4122 } else if let Some(cursor) = mc.get_primary_cursor() {
4123 Selection::Cursor(cursor)
4124 } else {
4125 return None;
4126 }
4127 } else {
4128 return None; };
4130
4131 crate::text3::edit::inspect_delete(&content, &selection, forward).map(|(range, text)| {
4133 DeleteResult {
4134 range_to_delete: range,
4135 deleted_text: text.into(),
4136 }
4137 })
4138 }
4139
4140 #[must_use] pub fn inspect_undo_operation(&self, node_id: NodeId) -> Option<&UndoableOperation> {
4145 self.get_undo_redo_manager().peek_undo(node_id)
4146 }
4147
4148 #[must_use] pub fn inspect_redo_operation(&self, node_id: NodeId) -> Option<&UndoableOperation> {
4152 self.get_undo_redo_manager().peek_redo(node_id)
4153 }
4154
4155 #[must_use] pub fn can_undo(&self, node_id: NodeId) -> bool {
4159 self.get_undo_redo_manager()
4160 .get_stack(node_id)
4161 .is_some_and(super::managers::undo_redo::NodeUndoRedoStack::can_undo)
4162 }
4163
4164 #[must_use] pub fn can_redo(&self, node_id: NodeId) -> bool {
4168 self.get_undo_redo_manager()
4169 .get_stack(node_id)
4170 .is_some_and(super::managers::undo_redo::NodeUndoRedoStack::can_redo)
4171 }
4172
4173 #[must_use] pub fn get_undo_text(&self, node_id: NodeId) -> Option<AzString> {
4178 self.get_undo_redo_manager()
4179 .peek_undo(node_id)
4180 .map(|op| op.pre_state.text_content.clone())
4181 }
4182
4183 #[must_use] pub fn get_redo_text(&self, node_id: NodeId) -> Option<AzString> {
4188 self.get_undo_redo_manager()
4189 .peek_redo(node_id)
4190 .map(|op| op.pre_state.text_content.clone())
4191 }
4192
4193 #[must_use] pub const fn get_clipboard_content(&self) -> Option<&ClipboardContent> {
4206 unsafe {
4207 (*self.ref_data)
4208 .layout_window
4209 .clipboard_manager
4210 .get_paste_content()
4211 }
4212 }
4213
4214 pub fn set_clipboard_content(&mut self, content: ClipboardContent) {
4222 self.set_copy_content(self.hit_dom_node, content);
4223 }
4224
4225 pub fn set_copy_content(&mut self, target: DomNodeId, content: ClipboardContent) {
4231 self.push_change(CallbackChange::SetCopyContent { target, content });
4232 }
4233
4234 pub fn set_cut_content(&mut self, target: DomNodeId, content: ClipboardContent) {
4239 self.push_change(CallbackChange::SetCutContent { target, content });
4240 }
4241
4242 pub fn set_select_all_range(&mut self, target: DomNodeId, range: SelectionRange) {
4247 self.push_change(CallbackChange::SetSelectAllRange { target, range });
4248 }
4249
4250 pub fn request_hit_test_update(&mut self, position: LogicalPosition) {
4258 self.push_change(CallbackChange::RequestHitTestUpdate { position });
4259 }
4260
4261 pub fn process_text_selection_click(&mut self, position: LogicalPosition, time_ms: u64) {
4269 self.push_change(CallbackChange::ProcessTextSelectionClick { position, time_ms });
4270 }
4271
4272 #[must_use] pub fn get_node_text_content(&self, target: DomNodeId) -> Option<String> {
4276 let layout_window = self.get_layout_window();
4277 let node_id = target.node.into_crate_internal()?;
4278 let exists = layout_window.dirty_text_nodes.contains_key(&(target.dom, node_id))
4284 || layout_window
4285 .layout_results
4286 .get(&target.dom)
4287 .is_some_and(|lr| node_id.index() < lr.styled_dom.node_data.as_ref().len());
4288 if !exists {
4289 return None;
4290 }
4291 let content = layout_window.get_text_before_textinput(target.dom, node_id);
4292 Some(layout_window.extract_text_from_inline_content(&content))
4293 }
4294
4295 #[must_use] pub fn get_node_cursor_position(&self, target: DomNodeId) -> Option<TextCursor> {
4299 let layout_window = self.get_layout_window();
4300
4301 if !layout_window.focus_manager.has_focus(&target) {
4303 return None;
4304 }
4305
4306 layout_window.text_edit_manager.get_primary_cursor()
4307 }
4308
4309 #[must_use] pub fn get_node_selection_ranges(&self, _target: DomNodeId) -> SelectionRangeVec {
4313 let layout_window = self.get_layout_window();
4314 let ranges: Vec<SelectionRange> = layout_window
4315 .text_edit_manager.multi_cursor.as_ref()
4316 .map(|mc| mc.selections.iter().filter_map(|s| match &s.selection {
4317 Selection::Range(r) => Some(*r),
4318 Selection::Cursor(_) => None,
4319 }).collect()).unwrap_or_default();
4320 ranges.into()
4321 }
4322
4323 #[must_use] pub fn node_has_selection(&self, target: DomNodeId) -> bool {
4328 !self.get_node_selection_ranges(target).as_ref().is_empty()
4329 }
4330
4331 #[must_use] pub fn get_node_text_length(&self, target: DomNodeId) -> Option<usize> {
4335 self.get_node_text_content(target).map(|text| text.len())
4336 }
4337
4338 pub fn inspect_move_cursor_left(&self, target: DomNodeId) -> Option<TextCursor> {
4348 let layout_window = self.get_layout_window();
4349 let cursor = layout_window.text_edit_manager.get_primary_cursor()?;
4350
4351 let layout = self.get_inline_layout_for_node(&target)?;
4354
4355 let new_cursor = layout.move_cursor_left(cursor, &mut None);
4357
4358 if new_cursor == cursor {
4360 None
4361 } else {
4362 Some(new_cursor)
4363 }
4364 }
4365
4366 pub fn inspect_move_cursor_right(&self, target: DomNodeId) -> Option<TextCursor> {
4371 let layout_window = self.get_layout_window();
4372 let cursor = layout_window.text_edit_manager.get_primary_cursor()?;
4373
4374 let layout = self.get_inline_layout_for_node(&target)?;
4377
4378 let new_cursor = layout.move_cursor_right(cursor, &mut None);
4380
4381 if new_cursor == cursor {
4383 None
4384 } else {
4385 Some(new_cursor)
4386 }
4387 }
4388
4389 pub fn inspect_move_cursor_up(&self, target: DomNodeId) -> Option<TextCursor> {
4394 let layout_window = self.get_layout_window();
4395 let cursor = layout_window.text_edit_manager.get_primary_cursor()?;
4396
4397 let layout = self.get_inline_layout_for_node(&target)?;
4400
4401 let new_cursor = layout.move_cursor_up(cursor, &mut None, &mut None);
4404
4405 if new_cursor == cursor {
4407 None
4408 } else {
4409 Some(new_cursor)
4410 }
4411 }
4412
4413 pub fn inspect_move_cursor_down(&self, target: DomNodeId) -> Option<TextCursor> {
4418 let layout_window = self.get_layout_window();
4419 let cursor = layout_window.text_edit_manager.get_primary_cursor()?;
4420
4421 let layout = self.get_inline_layout_for_node(&target)?;
4424
4425 let new_cursor = layout.move_cursor_down(cursor, &mut None, &mut None);
4428
4429 if new_cursor == cursor {
4431 None
4432 } else {
4433 Some(new_cursor)
4434 }
4435 }
4436
4437 pub fn inspect_move_cursor_to_line_start(&self, target: DomNodeId) -> Option<TextCursor> {
4441 let layout_window = self.get_layout_window();
4442 let cursor = layout_window.text_edit_manager.get_primary_cursor()?;
4443
4444 let layout = self.get_inline_layout_for_node(&target)?;
4447
4448 let new_cursor = layout.move_cursor_to_line_start(cursor, &mut None);
4450
4451 Some(new_cursor)
4453 }
4454
4455 pub fn inspect_move_cursor_to_line_end(&self, target: DomNodeId) -> Option<TextCursor> {
4459 let layout_window = self.get_layout_window();
4460 let cursor = layout_window.text_edit_manager.get_primary_cursor()?;
4461
4462 let layout = self.get_inline_layout_for_node(&target)?;
4465
4466 let new_cursor = layout.move_cursor_to_line_end(cursor, &mut None);
4468
4469 Some(new_cursor)
4471 }
4472
4473 #[must_use] pub const fn inspect_move_cursor_to_document_start(&self, target: DomNodeId) -> Option<TextCursor> {
4477 use azul_core::selection::{CursorAffinity, GraphemeClusterId};
4478
4479 Some(TextCursor {
4480 cluster_id: GraphemeClusterId {
4481 source_run: 0,
4482 start_byte_in_run: 0,
4483 },
4484 affinity: CursorAffinity::Leading,
4485 })
4486 }
4487
4488 #[must_use] pub fn inspect_move_cursor_to_document_end(&self, target: DomNodeId) -> Option<TextCursor> {
4492 use azul_core::selection::{CursorAffinity, GraphemeClusterId};
4493
4494 let text_len = self.get_node_text_length(target)?;
4495
4496 Some(TextCursor {
4497 cluster_id: GraphemeClusterId {
4498 source_run: 0,
4499 start_byte_in_run: u32::try_from(text_len).unwrap_or(u32::MAX),
4500 },
4501 affinity: CursorAffinity::Leading,
4502 })
4503 }
4504
4505 #[must_use] pub fn inspect_backspace(&self, target: DomNodeId) -> Option<DeleteResult> {
4510 self.inspect_delete_changeset(target, false)
4511 }
4512
4513 #[must_use] pub fn inspect_delete(&self, target: DomNodeId) -> Option<DeleteResult> {
4518 self.inspect_delete_changeset(target, true)
4519 }
4520
4521 pub fn move_cursor_left(&mut self, target: DomNodeId, extend_selection: bool) {
4530 self.push_change(CallbackChange::MoveCursorLeft {
4531 dom_id: target.dom,
4532 node_id: target.node.into_crate_internal().unwrap_or(NodeId::ZERO),
4533 extend_selection,
4534 });
4535 }
4536
4537 pub fn move_cursor_right(&mut self, target: DomNodeId, extend_selection: bool) {
4539 self.push_change(CallbackChange::MoveCursorRight {
4540 dom_id: target.dom,
4541 node_id: target.node.into_crate_internal().unwrap_or(NodeId::ZERO),
4542 extend_selection,
4543 });
4544 }
4545
4546 pub fn move_cursor_up(&mut self, target: DomNodeId, extend_selection: bool) {
4548 self.push_change(CallbackChange::MoveCursorUp {
4549 dom_id: target.dom,
4550 node_id: target.node.into_crate_internal().unwrap_or(NodeId::ZERO),
4551 extend_selection,
4552 });
4553 }
4554
4555 pub fn move_cursor_down(&mut self, target: DomNodeId, extend_selection: bool) {
4557 self.push_change(CallbackChange::MoveCursorDown {
4558 dom_id: target.dom,
4559 node_id: target.node.into_crate_internal().unwrap_or(NodeId::ZERO),
4560 extend_selection,
4561 });
4562 }
4563
4564 pub fn move_cursor_to_line_start(&mut self, target: DomNodeId, extend_selection: bool) {
4566 self.push_change(CallbackChange::MoveCursorToLineStart {
4567 dom_id: target.dom,
4568 node_id: target.node.into_crate_internal().unwrap_or(NodeId::ZERO),
4569 extend_selection,
4570 });
4571 }
4572
4573 pub fn move_cursor_to_line_end(&mut self, target: DomNodeId, extend_selection: bool) {
4575 self.push_change(CallbackChange::MoveCursorToLineEnd {
4576 dom_id: target.dom,
4577 node_id: target.node.into_crate_internal().unwrap_or(NodeId::ZERO),
4578 extend_selection,
4579 });
4580 }
4581
4582 pub fn move_cursor_to_document_start(&mut self, target: DomNodeId, extend_selection: bool) {
4584 self.push_change(CallbackChange::MoveCursorToDocumentStart {
4585 dom_id: target.dom,
4586 node_id: target.node.into_crate_internal().unwrap_or(NodeId::ZERO),
4587 extend_selection,
4588 });
4589 }
4590
4591 pub fn move_cursor_to_document_end(&mut self, target: DomNodeId, extend_selection: bool) {
4593 self.push_change(CallbackChange::MoveCursorToDocumentEnd {
4594 dom_id: target.dom,
4595 node_id: target.node.into_crate_internal().unwrap_or(NodeId::ZERO),
4596 extend_selection,
4597 });
4598 }
4599
4600 pub fn delete_backward(&mut self, target: DomNodeId) {
4605 self.push_change(CallbackChange::DeleteBackward {
4606 dom_id: target.dom,
4607 node_id: target.node.into_crate_internal().unwrap_or(NodeId::ZERO),
4608 });
4609 }
4610
4611 pub fn delete_forward(&mut self, target: DomNodeId) {
4616 self.push_change(CallbackChange::DeleteForward {
4617 dom_id: target.dom,
4618 node_id: target.node.into_crate_internal().unwrap_or(NodeId::ZERO),
4619 });
4620 }
4621}
4622
4623#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
4625#[repr(C)]
4626pub struct ExternalSystemCallbacks {
4627 pub create_thread_fn: CreateThreadCallback,
4628 pub get_system_time_fn: GetSystemTimeCallback,
4629}
4630
4631impl ExternalSystemCallbacks {
4632 #[must_use] pub fn rust_internal() -> Self {
4633 use crate::thread::create_thread_libstd;
4634
4635 Self {
4636 create_thread_fn: CreateThreadCallback {
4637 cb: create_thread_libstd,
4638 },
4639 get_system_time_fn: GetSystemTimeCallback {
4640 cb: task::get_system_time_libstd,
4641 },
4642 }
4643 }
4644}
4645
4646#[derive(Copy, Debug, Clone, PartialEq, Eq)]
4648pub enum FocusUpdateRequest {
4649 FocusNode(DomNodeId),
4651 ClearFocus,
4653 NoChange,
4655}
4656
4657impl FocusUpdateRequest {
4658 #[must_use] pub const fn is_change(&self) -> bool {
4660 !matches!(self, Self::NoChange)
4661 }
4662
4663 #[must_use] pub const fn to_focused_node(&self) -> Option<Option<DomNodeId>> {
4665 match self {
4666 Self::FocusNode(node) => Some(Some(*node)),
4667 Self::ClearFocus => Some(None),
4668 Self::NoChange => None,
4669 }
4670 }
4671
4672 #[must_use] pub const fn from_optional(opt: Option<Option<DomNodeId>>) -> Self {
4674 match opt {
4675 Some(Some(node)) => Self::FocusNode(node),
4676 Some(None) => Self::ClearFocus,
4677 None => Self::NoChange,
4678 }
4679 }
4680}
4681
4682#[derive(Debug, Clone, PartialEq, PartialOrd, Hash, Eq, Ord)]
4685#[repr(C)]
4686pub struct MenuCallback {
4687 pub callback: Callback,
4688 pub refany: RefAny,
4689}
4690#[allow(variant_size_differences)] #[derive(Debug, Clone, PartialEq, PartialOrd, Hash, Eq, Ord)]
4693#[repr(C, u8)]
4694pub enum OptionMenuCallback {
4695 None,
4696 Some(MenuCallback),
4697}
4698
4699impl OptionMenuCallback {
4700 #[must_use] pub fn into_option(self) -> Option<MenuCallback> {
4701 match self {
4702 Self::None => None,
4703 Self::Some(c) => Some(c),
4704 }
4705 }
4706
4707 #[must_use] pub const fn is_some(&self) -> bool {
4708 matches!(self, Self::Some(_))
4709 }
4710
4711 #[must_use] pub const fn is_none(&self) -> bool {
4712 matches!(self, Self::None)
4713 }
4714}
4715
4716impl From<Option<MenuCallback>> for OptionMenuCallback {
4717 fn from(o: Option<MenuCallback>) -> Self {
4718 o.map_or_else(|| Self::None, Self::Some)
4719 }
4720}
4721
4722impl From<OptionMenuCallback> for Option<MenuCallback> {
4723 fn from(o: OptionMenuCallback) -> Self {
4724 o.into_option()
4725 }
4726}
4727
4728pub type RenderImageCallbackType = extern "C" fn(RefAny, RenderImageCallbackInfo) -> ImageRef;
4736
4737#[repr(C)]
4744pub struct RenderImageCallback {
4745 pub cb: RenderImageCallbackType,
4746 pub ctx: OptionRefAny,
4749}
4750
4751impl_callback!(RenderImageCallback, RenderImageCallbackType);
4752
4753impl RenderImageCallback {
4754 pub fn create(cb: RenderImageCallbackType) -> Self {
4756 Self {
4757 cb,
4758 ctx: OptionRefAny::None,
4759 }
4760 }
4761
4762 #[must_use] pub fn from_core(core_callback: &azul_core::callbacks::CoreRenderImageCallback) -> Self {
4770 debug_assert!(core_callback.cb != 0, "CoreRenderImageCallback.cb is null");
4771 Self {
4772 cb: unsafe { core::mem::transmute::<usize, RenderImageCallbackType>(core_callback.cb) },
4773 ctx: core_callback.ctx.clone(),
4774 }
4775 }
4776
4777 #[must_use] pub fn to_core(self) -> azul_core::callbacks::CoreRenderImageCallback {
4781 azul_core::callbacks::CoreRenderImageCallback {
4782 cb: self.cb as usize,
4783 ctx: self.ctx,
4784 }
4785 }
4786}
4787
4788impl From<RenderImageCallback> for azul_core::callbacks::CoreRenderImageCallback {
4790 fn from(callback: RenderImageCallback) -> Self {
4791 callback.to_core()
4792 }
4793}
4794
4795#[derive(Debug)]
4797#[repr(C)]
4798pub struct RenderImageCallbackInfo {
4799 callback_node_id: DomNodeId,
4801 bounds: HidpiAdjustedBounds,
4803 gl_context: *const OptionGlContextPtr,
4805 image_cache: *const ImageCache,
4807 system_fonts: *const FcFontCache,
4809 callable_ptr: *const OptionRefAny,
4811 _abi_mut: *mut core::ffi::c_void,
4813}
4814
4815impl Clone for RenderImageCallbackInfo {
4816 #[allow(clippy::used_underscore_binding)]
4818 fn clone(&self) -> Self {
4819 Self {
4820 callback_node_id: self.callback_node_id,
4821 bounds: self.bounds,
4822 gl_context: self.gl_context,
4823 image_cache: self.image_cache,
4824 system_fonts: self.system_fonts,
4825 callable_ptr: self.callable_ptr,
4826 _abi_mut: self._abi_mut,
4827 }
4828 }
4829}
4830
4831impl RenderImageCallbackInfo {
4832 #[must_use] pub const fn new<'a>(
4833 callback_node_id: DomNodeId,
4834 bounds: HidpiAdjustedBounds,
4835 gl_context: &'a OptionGlContextPtr,
4836 image_cache: &'a ImageCache,
4837 system_fonts: &'a FcFontCache,
4838 ) -> Self {
4839 Self {
4840 callback_node_id,
4841 bounds,
4842 gl_context: std::ptr::from_ref::<OptionGlContextPtr>(gl_context),
4843 image_cache: std::ptr::from_ref::<ImageCache>(image_cache),
4844 system_fonts: std::ptr::from_ref::<FcFontCache>(system_fonts),
4845 callable_ptr: core::ptr::null(),
4846 _abi_mut: core::ptr::null_mut(),
4847 }
4848 }
4849
4850 #[must_use] pub fn get_ctx(&self) -> OptionRefAny {
4852 if self.callable_ptr.is_null() {
4853 OptionRefAny::None
4854 } else {
4855 unsafe { (*self.callable_ptr).clone() }
4856 }
4857 }
4858
4859 pub const unsafe fn set_callable_ptr(&mut self, ptr: *const OptionRefAny) {
4870 self.callable_ptr = ptr;
4871 }
4872
4873 #[must_use] pub const fn get_callback_node_id(&self) -> DomNodeId {
4874 self.callback_node_id
4875 }
4876
4877 #[must_use] pub const fn get_bounds(&self) -> HidpiAdjustedBounds {
4878 self.bounds
4879 }
4880
4881 const fn internal_get_gl_context(&self) -> &OptionGlContextPtr {
4882 unsafe { &*self.gl_context }
4883 }
4884
4885 const fn internal_get_image_cache(&self) -> &ImageCache {
4886 unsafe { &*self.image_cache }
4887 }
4888
4889 const fn internal_get_system_fonts(&self) -> &FcFontCache {
4890 unsafe { &*self.system_fonts }
4891 }
4892
4893 #[must_use] pub fn get_gl_context(&self) -> OptionGlContextPtr {
4894 self.internal_get_gl_context().clone()
4895 }
4896}
4897
4898#[derive(Debug, Clone)]
4904#[repr(C, u8)]
4905pub enum ResultU8VecString {
4906 Ok(U8Vec),
4907 Err(AzString),
4908}
4909
4910impl From<Result<alloc::vec::Vec<u8>, AzString>> for ResultU8VecString {
4911 fn from(result: Result<alloc::vec::Vec<u8>, AzString>) -> Self {
4912 match result {
4913 Ok(v) => Self::Ok(v.into()),
4914 Err(e) => Self::Err(e),
4915 }
4916 }
4917}
4918#[allow(variant_size_differences)] #[derive(Debug, Clone)]
4921#[repr(C, u8)]
4922pub enum ResultVoidString {
4923 Ok,
4924 Err(AzString),
4925}
4926
4927impl From<Result<(), AzString>> for ResultVoidString {
4928 fn from(result: Result<(), AzString>) -> Self {
4929 match result {
4930 Ok(()) => Self::Ok,
4931 Err(e) => Self::Err(e),
4932 }
4933 }
4934}
4935
4936#[derive(Debug, Clone)]
4938#[repr(C, u8)]
4939pub enum ResultStringString {
4940 Ok(AzString),
4941 Err(AzString),
4942}
4943
4944impl From<Result<AzString, AzString>> for ResultStringString {
4945 fn from(result: Result<AzString, AzString>) -> Self {
4946 match result {
4947 Ok(s) => Self::Ok(s),
4948 Err(e) => Self::Err(e),
4949 }
4950 }
4951}
4952
4953const BASE64_ALPHABET: &[u8; 64] =
4958 b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
4959
4960#[must_use] pub fn base64_encode(input: &[u8]) -> String {
4962 let mut output = String::with_capacity(input.len().div_ceil(3) * 4);
4963
4964 for chunk in input.chunks(3) {
4965 let b0 = chunk[0] as usize;
4966 let b1 = chunk.get(1).copied().unwrap_or(0) as usize;
4967 let b2 = chunk.get(2).copied().unwrap_or(0) as usize;
4968
4969 let n = (b0 << 16) | (b1 << 8) | b2;
4970
4971 output.push(BASE64_ALPHABET[(n >> 18) & 0x3F] as char);
4972 output.push(BASE64_ALPHABET[(n >> 12) & 0x3F] as char);
4973
4974 if chunk.len() > 1 {
4975 output.push(BASE64_ALPHABET[(n >> 6) & 0x3F] as char);
4976 } else {
4977 output.push('=');
4978 }
4979
4980 if chunk.len() > 2 {
4981 output.push(BASE64_ALPHABET[n & 0x3F] as char);
4982 } else {
4983 output.push('=');
4984 }
4985 }
4986
4987 output
4988}
4989
4990#[cfg(all(test, feature = "std"))]
4991#[allow(clippy::float_cmp, clippy::cast_possible_truncation)]
4992mod autotest_generated {
4993 use super::*;
4994
4995 fn with_info<R>(hit: DomNodeId, f: impl FnOnce(&mut CallbackInfo) -> R) -> R {
5004 let layout_window =
5005 LayoutWindow::new(FcFontCache::default()).expect("LayoutWindow::new failed");
5006 let renderer_resources = RendererResources::default();
5007 let previous_window_state: Option<FullWindowState> = None;
5008 let current_window_state = FullWindowState::default();
5009 let gl_context = OptionGlContextPtr::None;
5010 let scroll_states: BTreeMap<DomId, BTreeMap<NodeHierarchyItemId, ScrollPosition>> =
5011 BTreeMap::new();
5012 let window_handle = RawWindowHandle::Unsupported;
5013 let system_callbacks = ExternalSystemCallbacks::rust_internal();
5014
5015 let ref_data = CallbackInfoRefData {
5016 layout_window: &layout_window,
5017 renderer_resources: &renderer_resources,
5018 previous_window_state: &previous_window_state,
5019 current_window_state: ¤t_window_state,
5020 gl_context: &gl_context,
5021 current_scroll_manager: &scroll_states,
5022 current_window_handle: &window_handle,
5023 system_callbacks: &system_callbacks,
5024 system_style: Arc::new(SystemStyle::default()),
5025 monitors: Arc::new(std::sync::Mutex::new(MonitorVec::from_const_slice(&[]))),
5026 #[cfg(feature = "icu")]
5027 icu_localizer: IcuLocalizerHandle::default(),
5028 ctx: OptionRefAny::None,
5029 };
5030
5031 let changes: Arc<std::sync::Mutex<Vec<CallbackChange>>> =
5032 Arc::new(std::sync::Mutex::new(Vec::new()));
5033
5034 let mut info = CallbackInfo::new(
5035 &ref_data,
5036 &changes,
5037 hit,
5038 OptionLogicalPosition::None,
5039 OptionLogicalPosition::None,
5040 );
5041
5042 f(&mut info)
5043 }
5044
5045 fn node0() -> DomNodeId {
5047 DomNodeId {
5048 dom: DomId::ROOT_ID,
5049 node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(0))),
5050 }
5051 }
5052
5053 fn node_none() -> DomNodeId {
5055 DomNodeId {
5056 dom: DomId::ROOT_ID,
5057 node: NodeHierarchyItemId::NONE,
5058 }
5059 }
5060
5061 extern "C" fn cb_do_nothing(_: RefAny, _: CallbackInfo) -> Update {
5062 Update::DoNothing
5063 }
5064
5065 extern "C" fn cb_refresh_dom(_: RefAny, _: CallbackInfo) -> Update {
5066 Update::RefreshDom
5067 }
5068
5069 extern "C" fn cb_pushes_change(_: RefAny, mut info: CallbackInfo) -> Update {
5071 info.stop_propagation();
5072 Update::RefreshDomAllWindows
5073 }
5074
5075 extern "C" fn img_cb(_: RefAny, _: RenderImageCallbackInfo) -> ImageRef {
5076 ImageRef::null_image(0, 0, azul_core::resources::RawImageFormat::RGBA8, Vec::new())
5077 }
5078
5079 fn a_css_property() -> CssProperty {
5080 use azul_css::props::{basic::PixelValue, layout::dimensions::LayoutWidth};
5081 CssProperty::const_width(LayoutWidth::Px(PixelValue::px(123.0)))
5082 }
5083
5084 fn a_cursor() -> TextCursor {
5085 use azul_core::selection::{CursorAffinity, GraphemeClusterId};
5086 TextCursor {
5087 cluster_id: GraphemeClusterId {
5088 source_run: 0,
5089 start_byte_in_run: 0,
5090 },
5091 affinity: CursorAffinity::Leading,
5092 }
5093 }
5094
5095 fn base64_decode(s: &str) -> Option<Vec<u8>> {
5102 fn val(c: u8) -> Option<u32> {
5103 match c {
5104 b'A'..=b'Z' => Some((c - b'A') as u32),
5105 b'a'..=b'z' => Some((c - b'a') as u32 + 26),
5106 b'0'..=b'9' => Some((c - b'0') as u32 + 52),
5107 b'+' => Some(62),
5108 b'/' => Some(63),
5109 _ => None,
5110 }
5111 }
5112
5113 let bytes = s.as_bytes();
5114 if bytes.len() % 4 != 0 {
5115 return None;
5116 }
5117 let mut out = Vec::with_capacity(bytes.len() / 4 * 3);
5118 for chunk in bytes.chunks(4) {
5119 let pad = chunk.iter().filter(|&&c| c == b'=').count();
5120 if pad > 2 {
5121 return None;
5122 }
5123 let mut n: u32 = 0;
5124 for (i, &c) in chunk.iter().enumerate() {
5125 let v = if c == b'=' { 0 } else { val(c)? };
5126 n |= v << (18 - 6 * i as u32);
5127 }
5128 out.push(((n >> 16) & 0xFF) as u8);
5129 if pad < 2 {
5130 out.push(((n >> 8) & 0xFF) as u8);
5131 }
5132 if pad < 1 {
5133 out.push((n & 0xFF) as u8);
5134 }
5135 }
5136 Some(out)
5137 }
5138
5139 #[test]
5140 fn base64_encode_rfc4648_test_vectors() {
5141 assert_eq!(base64_encode(b""), "");
5142 assert_eq!(base64_encode(b"f"), "Zg==");
5143 assert_eq!(base64_encode(b"fo"), "Zm8=");
5144 assert_eq!(base64_encode(b"foo"), "Zm9v");
5145 assert_eq!(base64_encode(b"foob"), "Zm9vYg==");
5146 assert_eq!(base64_encode(b"fooba"), "Zm9vYmE=");
5147 assert_eq!(base64_encode(b"foobar"), "Zm9vYmFy");
5148 }
5149
5150 #[test]
5151 fn base64_encode_extreme_bytes() {
5152 assert_eq!(base64_encode(&[0x00, 0x00, 0x00]), "AAAA");
5154 assert_eq!(base64_encode(&[0xFF, 0xFF, 0xFF]), "////");
5155 assert_eq!(base64_encode(&[0xFF]), "/w==");
5157 assert_eq!(base64_encode(&[0xFF, 0xFF]), "//8=");
5158 let all: Vec<u8> = (0u8..=255).collect();
5160 let enc = base64_encode(&all);
5161 assert_eq!(base64_decode(&enc).as_deref(), Some(all.as_slice()));
5162 }
5163
5164 #[test]
5165 fn base64_encode_output_length_is_ceil_div_3_times_4() {
5166 for n in 0usize..=64 {
5167 let input = vec![0xABu8; n];
5168 let enc = base64_encode(&input);
5169 assert_eq!(
5170 enc.len(),
5171 n.div_ceil(3) * 4,
5172 "unexpected encoded length for {n} input bytes"
5173 );
5174 let pad = enc.bytes().filter(|&c| c == b'=').count();
5176 assert!(pad <= 2, "too much padding for n = {n}");
5177 assert_eq!(pad, (3 - n % 3) % 3, "wrong padding count for n = {n}");
5178 if pad > 0 {
5179 assert!(enc.ends_with(&"=".repeat(pad)));
5180 }
5181 }
5182 }
5183
5184 #[test]
5185 fn base64_encode_emits_only_alphabet_characters() {
5186 let input: Vec<u8> = (0u8..=255).chain(0u8..=255).collect();
5187 let enc = base64_encode(&input);
5188 for c in enc.bytes() {
5189 assert!(
5190 c == b'=' || BASE64_ALPHABET.contains(&c),
5191 "non-base64 char {c:?} in output"
5192 );
5193 }
5194 }
5195
5196 #[test]
5197 fn base64_encode_round_trips_for_every_length_remainder() {
5198 for n in 0usize..=130 {
5200 let input: Vec<u8> = (0..n).map(|i| (i * 7 + 13) as u8).collect();
5201 let enc = base64_encode(&input);
5202 let dec = base64_decode(&enc).unwrap_or_else(|| panic!("failed to decode {enc:?}"));
5203 assert_eq!(dec, input, "round-trip failed at length {n}");
5204 }
5205 }
5206
5207 #[test]
5208 fn base64_encode_unicode_bytes_round_trip() {
5209 for s in [
5210 "\u{1F600}", "e\u{301}", "\u{0}\u{7F}\u{80}\u{FFFF}", "тест 日本語 🌍",
5214 ] {
5215 let enc = base64_encode(s.as_bytes());
5216 assert_eq!(base64_decode(&enc).as_deref(), Some(s.as_bytes()));
5217 }
5218 assert_eq!(base64_encode("\u{1F600}".as_bytes()), "8J+YgA==");
5220 }
5221
5222 #[test]
5223 fn base64_encode_one_megabyte_does_not_panic_or_hang() {
5224 let input = vec![0x5Au8; 1_000_000];
5225 let enc = base64_encode(&input);
5226 assert_eq!(enc.len(), 1_000_000usize.div_ceil(3) * 4);
5227 assert!(enc.ends_with("=="));
5229 assert_eq!(base64_decode(&enc).map(|v| v.len()), Some(1_000_000));
5230 }
5231
5232 #[test]
5237 fn pen_tilt_from_tuple_preserves_extreme_floats() {
5238 let t = PenTilt::from((0.0, -0.0));
5239 assert_eq!(t.x_tilt, 0.0);
5240 assert!(t.y_tilt.is_sign_negative());
5241
5242 let t = PenTilt::from((f32::MAX, f32::MIN));
5243 assert_eq!(t.x_tilt, f32::MAX);
5244 assert_eq!(t.y_tilt, f32::MIN);
5245
5246 let t = PenTilt::from((f32::INFINITY, f32::NEG_INFINITY));
5247 assert!(t.x_tilt.is_infinite() && t.x_tilt.is_sign_positive());
5248 assert!(t.y_tilt.is_infinite() && t.y_tilt.is_sign_negative());
5249
5250 let t = PenTilt::from((f32::NAN, 90.0));
5253 assert!(t.x_tilt.is_nan());
5254 assert_eq!(t.y_tilt, 90.0);
5255 assert_ne!(t, t);
5256 }
5257
5258 #[test]
5259 fn option_pen_tilt_is_some_is_none_are_exclusive() {
5260 let some = OptionPenTilt::Some(PenTilt::from((1.0, 2.0)));
5261 let none = OptionPenTilt::None;
5262 assert!(some.is_some() && !some.is_none());
5263 assert!(none.is_none() && !none.is_some());
5264 }
5265
5266 #[test]
5267 fn select_all_result_from_tuple_keeps_fields_including_empty_and_huge() {
5268 let range = SelectionRange {
5269 start: a_cursor(),
5270 end: a_cursor(),
5271 };
5272
5273 let empty = SelectAllResult::from((String::new(), range));
5274 assert_eq!(empty.full_text.as_str(), "");
5275 assert_eq!(empty.selection_range, range);
5276
5277 let huge = SelectAllResult::from(("x".repeat(100_000), range));
5278 assert_eq!(huge.full_text.as_str().len(), 100_000);
5279
5280 let unicode = SelectAllResult::from(("🌍\u{0}é".to_string(), range));
5281 assert_eq!(unicode.full_text.as_str(), "🌍\u{0}é");
5282 }
5283
5284 #[test]
5285 fn delete_result_from_tuple_keeps_fields() {
5286 let range = SelectionRange {
5287 start: a_cursor(),
5288 end: a_cursor(),
5289 };
5290 let d = DeleteResult::from((range, String::new()));
5291 assert_eq!(d.range_to_delete, range);
5292 assert_eq!(d.deleted_text.as_str(), "");
5293
5294 let d = DeleteResult::from((range, "\u{1F600}".to_string()));
5295 assert_eq!(d.deleted_text.as_str(), "\u{1F600}");
5296 }
5297
5298 #[test]
5303 fn callback_from_ptr_and_create_and_from_agree() {
5304 let a = Callback::from_ptr(cb_do_nothing);
5305 let b = Callback::create(cb_do_nothing as CallbackType);
5306 let c = Callback::from(cb_do_nothing as CallbackType);
5307
5308 assert_eq!(a, b);
5309 assert_eq!(b, c);
5310 assert!(a.ctx.is_none());
5312 assert!(b.ctx.is_none());
5313 assert!(c.ctx.is_none());
5314 assert_ne!(a.cb as usize, 0);
5315 }
5316
5317 #[test]
5318 fn callback_to_core_from_core_round_trips_pointer_and_ctx() {
5319 let original = Callback {
5320 cb: cb_refresh_dom,
5321 ctx: OptionRefAny::Some(RefAny::new(0xDEAD_BEEFu32)),
5322 };
5323 let ptr = original.cb as usize;
5324
5325 let core = original.to_core();
5326 assert_eq!(core.cb, ptr);
5327 assert!(core.ctx.is_some(), "to_core must not drop the FFI ctx");
5328
5329 let back = Callback::from_core(core);
5330 assert_eq!(back.cb as usize, ptr, "encode == decode for the fn pointer");
5331 assert!(
5332 back.ctx.is_some(),
5333 "from_core must preserve ctx (managed-FFI handlers rely on it)"
5334 );
5335 }
5336
5337 #[test]
5338 fn callback_to_core_of_ctxless_callback_keeps_ctx_none() {
5339 let core = Callback::from_ptr(cb_do_nothing).to_core();
5340 assert!(core.ctx.is_none());
5341 assert_eq!(Callback::from_core(core).cb as usize, cb_do_nothing as usize);
5342 }
5343
5344 #[test]
5345 #[cfg(debug_assertions)]
5346 #[should_panic(expected = "CoreCallback.cb is null")]
5347 fn callback_from_core_null_pointer_trips_debug_assert() {
5348 let _ = Callback::from_core(CoreCallback {
5351 cb: 0,
5352 ctx: OptionRefAny::None,
5353 });
5354 }
5355
5356 #[test]
5357 fn callback_invoke_returns_the_functions_update() {
5358 let update = with_info(node_none(), |info| {
5359 Callback::from_ptr(cb_refresh_dom).invoke(RefAny::new(1u8), *info)
5360 });
5361 assert!(matches!(update, Update::RefreshDom));
5362
5363 let update = with_info(node_none(), |info| {
5364 Callback::from_ptr(cb_do_nothing).invoke(RefAny::new(1u8), *info)
5365 });
5366 assert!(matches!(update, Update::DoNothing));
5367 }
5368
5369 #[test]
5370 fn callback_invoke_changes_reach_the_callers_transaction_log() {
5371 let changes = with_info(node_none(), |info| {
5374 let update = Callback::from_ptr(cb_pushes_change).invoke(RefAny::new(0u8), *info);
5375 assert!(matches!(update, Update::RefreshDomAllWindows));
5376 info.take_changes()
5377 });
5378 assert_eq!(changes.len(), 1);
5379 assert!(matches!(changes[0], CallbackChange::StopPropagation));
5380 }
5381
5382 #[test]
5383 fn callback_eq_and_hash_ignore_ctx_but_stay_consistent() {
5384 use std::{
5385 collections::hash_map::DefaultHasher,
5386 hash::{Hash, Hasher},
5387 };
5388
5389 let plain = Callback::from_ptr(cb_do_nothing);
5390 let with_ctx = Callback {
5391 cb: cb_do_nothing,
5392 ctx: OptionRefAny::Some(RefAny::new(7u64)),
5393 };
5394 let other_fn = Callback::from_ptr(cb_refresh_dom);
5395
5396 assert_eq!(plain, with_ctx);
5398 assert_ne!(plain, other_fn);
5399
5400 let hash = |c: &Callback| {
5402 let mut h = DefaultHasher::new();
5403 c.hash(&mut h);
5404 h.finish()
5405 };
5406 assert_eq!(hash(&plain), hash(&with_ctx));
5407 }
5408
5409 #[test]
5414 fn option_callback_predicates_are_exclusive_and_total() {
5415 let none = OptionCallback::None;
5416 let some = OptionCallback::Some(Callback::from_ptr(cb_do_nothing));
5417
5418 assert!(none.is_none() && !none.is_some());
5419 assert!(some.is_some() && !some.is_none());
5420 for v in [&none, &some] {
5422 assert!(v.is_some() ^ v.is_none());
5423 }
5424 }
5425
5426 #[test]
5427 fn option_callback_round_trips_through_std_option() {
5428 let cb = Callback::from_ptr(cb_do_nothing);
5429
5430 let round = OptionCallback::from(Some(cb.clone())).into_option();
5431 assert_eq!(round, Some(cb.clone()));
5432
5433 let round = OptionCallback::from(None).into_option();
5434 assert_eq!(round, None);
5435
5436 let ffi: OptionCallback = Some(cb.clone()).into();
5438 let back: Option<Callback> = ffi.into();
5439 assert_eq!(back, Some(cb));
5440
5441 let ffi: OptionCallback = None.into();
5442 let back: Option<Callback> = ffi.into();
5443 assert_eq!(back, None);
5444 }
5445
5446 #[test]
5447 fn option_menu_callback_predicates_and_round_trip() {
5448 let mc = MenuCallback {
5449 callback: Callback::from_ptr(cb_do_nothing),
5450 refany: RefAny::new(5i32),
5451 };
5452
5453 let none = OptionMenuCallback::None;
5454 assert!(none.is_none() && !none.is_some());
5455 assert_eq!(none.into_option(), None);
5456
5457 let some = OptionMenuCallback::from(Some(mc.clone()));
5458 assert!(some.is_some() && !some.is_none());
5459 assert_eq!(some.into_option(), Some(mc));
5460
5461 let back: Option<MenuCallback> = OptionMenuCallback::None.into();
5462 assert!(back.is_none());
5463 }
5464
5465 #[test]
5470 fn render_image_callback_core_round_trip() {
5471 let cb = RenderImageCallback::create(img_cb);
5472 assert!(cb.ctx.is_none());
5473 let ptr = cb.cb as usize;
5474
5475 let core = cb.to_core();
5476 assert_eq!(core.cb, ptr);
5477
5478 let back = RenderImageCallback::from_core(&core);
5479 assert_eq!(back.cb as usize, ptr);
5480 assert!(back.ctx.is_none());
5481 }
5482
5483 #[test]
5484 #[cfg(debug_assertions)]
5485 #[should_panic(expected = "CoreRenderImageCallback.cb is null")]
5486 fn render_image_callback_from_core_null_pointer_trips_debug_assert() {
5487 let core = azul_core::callbacks::CoreRenderImageCallback {
5488 cb: 0,
5489 ctx: OptionRefAny::None,
5490 };
5491 let _ = RenderImageCallback::from_core(&core);
5492 }
5493
5494 #[test]
5495 fn render_image_callback_info_getters_and_null_ctx() {
5496 let gl = OptionGlContextPtr::None;
5497 let image_cache = ImageCache::default();
5498 let fonts = FcFontCache::default();
5499 let bounds = HidpiAdjustedBounds {
5500 logical_size: LogicalSize::new(640.0, 480.0),
5501 hidpi_factor: azul_core::resources::DpiScaleFactor::new(2.0),
5502 };
5503
5504 let info = RenderImageCallbackInfo::new(node0(), bounds, &gl, &image_cache, &fonts);
5505
5506 assert_eq!(info.get_callback_node_id(), node0());
5507 assert_eq!(info.get_bounds().logical_size, LogicalSize::new(640.0, 480.0));
5508 assert!(info.get_ctx().is_none());
5511 assert!(info.get_gl_context().is_none());
5512
5513 let cloned = info.clone();
5515 assert_eq!(cloned.get_callback_node_id(), node0());
5516 assert!(cloned.get_ctx().is_none());
5517 }
5518
5519 #[test]
5520 fn render_image_callback_info_accepts_degenerate_and_nan_bounds() {
5521 let gl = OptionGlContextPtr::None;
5522 let image_cache = ImageCache::default();
5523 let fonts = FcFontCache::default();
5524
5525 for (w, h, dpi) in [
5526 (0.0f32, 0.0f32, 0.0f32),
5527 (-1.0, -1.0, 1.0),
5528 (f32::MAX, f32::MAX, f32::MAX),
5529 (f32::INFINITY, f32::NAN, 1.0),
5530 ] {
5531 let bounds = HidpiAdjustedBounds {
5532 logical_size: LogicalSize::new(w, h),
5533 hidpi_factor: azul_core::resources::DpiScaleFactor::new(dpi),
5534 };
5535 let info = RenderImageCallbackInfo::new(node_none(), bounds, &gl, &image_cache, &fonts);
5536 let got = info.get_bounds().logical_size;
5537 assert_eq!(got.width.is_nan(), w.is_nan());
5538 assert_eq!(got.height.is_nan(), h.is_nan());
5539 }
5540 }
5541
5542 #[test]
5543 fn render_image_callback_info_set_callable_ptr_makes_ctx_visible() {
5544 let gl = OptionGlContextPtr::None;
5545 let image_cache = ImageCache::default();
5546 let fonts = FcFontCache::default();
5547 let bounds = HidpiAdjustedBounds {
5548 logical_size: LogicalSize::new(1.0, 1.0),
5549 hidpi_factor: azul_core::resources::DpiScaleFactor::new(1.0),
5550 };
5551 let mut info = RenderImageCallbackInfo::new(node0(), bounds, &gl, &image_cache, &fonts);
5552
5553 let ctx = OptionRefAny::Some(RefAny::new(99u32));
5554 unsafe { info.set_callable_ptr(core::ptr::from_ref(&ctx)) };
5556 assert!(info.get_ctx().is_some());
5557
5558 unsafe { info.set_callable_ptr(core::ptr::null()) };
5560 assert!(info.get_ctx().is_none());
5561 }
5562
5563 #[test]
5568 fn focus_update_request_is_change_matches_variant() {
5569 assert!(FocusUpdateRequest::FocusNode(node0()).is_change());
5570 assert!(FocusUpdateRequest::ClearFocus.is_change());
5571 assert!(!FocusUpdateRequest::NoChange.is_change());
5572 }
5573
5574 #[test]
5575 fn focus_update_request_optional_round_trip_is_lossless() {
5576 for req in [
5577 FocusUpdateRequest::FocusNode(node0()),
5578 FocusUpdateRequest::FocusNode(node_none()),
5579 FocusUpdateRequest::ClearFocus,
5580 FocusUpdateRequest::NoChange,
5581 ] {
5582 assert_eq!(
5583 FocusUpdateRequest::from_optional(req.to_focused_node()),
5584 req,
5585 "from_optional . to_focused_node must be the identity"
5586 );
5587 }
5588
5589 for opt in [Some(Some(node0())), Some(None), None] {
5591 assert_eq!(FocusUpdateRequest::from_optional(opt).to_focused_node(), opt);
5592 }
5593
5594 for req in [
5596 FocusUpdateRequest::FocusNode(node0()),
5597 FocusUpdateRequest::ClearFocus,
5598 FocusUpdateRequest::NoChange,
5599 ] {
5600 assert_eq!(req.is_change(), req.to_focused_node().is_some());
5601 }
5602 }
5603
5604 #[test]
5609 fn result_u8vec_string_from_maps_ok_and_err() {
5610 let ok = ResultU8VecString::from(Ok(Vec::new()));
5611 assert!(matches!(&ok, ResultU8VecString::Ok(v) if v.is_empty()));
5612
5613 let ok = ResultU8VecString::from(Ok(vec![0u8; 100_000]));
5614 assert!(matches!(&ok, ResultU8VecString::Ok(v) if v.len() == 100_000));
5615
5616 let err = ResultU8VecString::from(Err(AzString::from("boom")));
5617 assert!(matches!(&err, ResultU8VecString::Err(e) if e.as_str() == "boom"));
5618 }
5619
5620 #[test]
5621 fn result_void_string_from_maps_ok_and_err() {
5622 assert!(matches!(ResultVoidString::from(Ok(())), ResultVoidString::Ok));
5623 let err = ResultVoidString::from(Err(AzString::from("")));
5624 assert!(matches!(&err, ResultVoidString::Err(e) if e.as_str().is_empty()));
5625 }
5626
5627 #[test]
5628 fn result_string_string_from_keeps_both_sides_distinct() {
5629 let ok = ResultStringString::from(Ok(AzString::from("x")));
5630 assert!(matches!(&ok, ResultStringString::Ok(s) if s.as_str() == "x"));
5631 let err = ResultStringString::from(Err(AzString::from("x")));
5633 assert!(matches!(&err, ResultStringString::Err(s) if s.as_str() == "x"));
5634 }
5635
5636 #[test]
5641 fn external_system_callbacks_time_fn_is_callable_and_monotonic() {
5642 let cbs = ExternalSystemCallbacks::rust_internal();
5643 let t0 = (cbs.get_system_time_fn.cb)();
5644 let t1 = (cbs.get_system_time_fn.cb)();
5645 let _ = (t0, t1);
5648 }
5649
5650 #[test]
5655 fn callback_info_starts_with_an_empty_change_log() {
5656 with_info(node_none(), |info| {
5657 assert!(info.take_changes().is_empty());
5658 assert!(!info.has_pending_relayout_change());
5659 assert!(!info.get_changes_ptr().is_null());
5660 });
5661 }
5662
5663 #[test]
5664 fn callback_info_take_changes_drains_the_log() {
5665 with_info(node_none(), |info| {
5666 info.stop_propagation();
5667 info.prevent_default();
5668 let first = info.take_changes();
5669 assert_eq!(first.len(), 2);
5670 assert!(
5672 info.take_changes().is_empty(),
5673 "take_changes must consume the log"
5674 );
5675 });
5676 }
5677
5678 #[test]
5679 fn callback_info_is_copy_and_copies_share_one_change_log() {
5680 with_info(node_none(), |info| {
5681 let mut copy = *info;
5682 copy.stop_immediate_propagation();
5683 assert_eq!(
5684 info.get_changes_ptr(),
5685 copy.get_changes_ptr(),
5686 "a Copy of CallbackInfo must alias the same Arc<Mutex<..>>"
5687 );
5688 let changes = info.take_changes();
5689 assert_eq!(changes.len(), 1);
5690 assert!(matches!(changes[0], CallbackChange::StopImmediatePropagation));
5691 });
5692 }
5693
5694 #[test]
5695 fn has_pending_relayout_change_is_true_only_for_relayout_changes() {
5696 with_info(node_none(), |info| {
5698 info.stop_propagation();
5699 assert!(!info.has_pending_relayout_change());
5700 });
5701 with_info(node_none(), |info| {
5703 info.modify_window_state(FullWindowState::default());
5704 assert!(info.has_pending_relayout_change());
5705 });
5706 with_info(node_none(), |info| {
5708 info.scroll_to(
5709 DomId::ROOT_ID,
5710 NodeHierarchyItemId::NONE,
5711 LogicalPosition::new(0.0, 0.0),
5712 );
5713 assert!(info.has_pending_relayout_change());
5714 });
5715 with_info(node_none(), |info| {
5717 info.queue_window_state_sequence(FullWindowStateVec::from_vec(vec![
5718 FullWindowState::default(),
5719 ]));
5720 assert!(info.has_pending_relayout_change());
5721 });
5722 with_info(node_none(), |info| {
5724 info.prevent_default();
5725 info.hide_tooltip();
5726 info.close_window();
5727 assert!(!info.has_pending_relayout_change());
5728 info.modify_window_state(FullWindowState::default());
5729 assert!(info.has_pending_relayout_change());
5730 assert!(info.has_pending_relayout_change());
5732 assert_eq!(info.take_changes().len(), 4);
5733 });
5734 }
5735
5736 #[test]
5737 fn callback_info_flag_mutators_queue_exactly_one_matching_change() {
5738 macro_rules! assert_queues {
5739 ($call:expr, $pat:pat) => {{
5740 with_info(node_none(), |info| {
5741 let f: &dyn Fn(&mut CallbackInfo) = &$call;
5742 f(info);
5743 let changes = info.take_changes();
5744 assert_eq!(changes.len(), 1, "expected exactly one queued change");
5745 assert!(
5746 matches!(changes[0], $pat),
5747 "queued the wrong CallbackChange: {:?}",
5748 changes[0]
5749 );
5750 });
5751 }};
5752 }
5753
5754 assert_queues!(
5755 |i: &mut CallbackInfo| i.stop_propagation(),
5756 CallbackChange::StopPropagation
5757 );
5758 assert_queues!(
5759 |i: &mut CallbackInfo| i.stop_immediate_propagation(),
5760 CallbackChange::StopImmediatePropagation
5761 );
5762 assert_queues!(
5763 |i: &mut CallbackInfo| i.prevent_default(),
5764 CallbackChange::PreventDefault
5765 );
5766 assert_queues!(
5767 |i: &mut CallbackInfo| i.close_window(),
5768 CallbackChange::CloseWindow
5769 );
5770 assert_queues!(
5771 |i: &mut CallbackInfo| i.begin_interactive_move(),
5772 CallbackChange::BeginInteractiveMove
5773 );
5774 assert_queues!(
5775 |i: &mut CallbackInfo| i.commit_undo_snapshot(),
5776 CallbackChange::CommitUndoSnapshot
5777 );
5778 assert_queues!(
5779 |i: &mut CallbackInfo| i.undo_app_state(),
5780 CallbackChange::UndoAppState
5781 );
5782 assert_queues!(
5783 |i: &mut CallbackInfo| i.redo_app_state(),
5784 CallbackChange::RedoAppState
5785 );
5786 assert_queues!(
5787 |i: &mut CallbackInfo| i.update_all_image_callbacks(),
5788 CallbackChange::UpdateAllImageCallbacks
5789 );
5790 assert_queues!(
5791 |i: &mut CallbackInfo| i.trigger_all_virtual_view_rerender(),
5792 CallbackChange::UpdateAllVirtualViews
5793 );
5794 assert_queues!(
5795 |i: &mut CallbackInfo| i.reload_system_fonts(),
5796 CallbackChange::ReloadSystemFonts
5797 );
5798 assert_queues!(
5799 |i: &mut CallbackInfo| i.hide_tooltip(),
5800 CallbackChange::HideTooltip
5801 );
5802 }
5803
5804 #[test]
5805 fn callback_info_timer_and_thread_ids_survive_boundary_values() {
5806 with_info(node_none(), |info| {
5807 info.add_timer(TimerId { id: 0 }, Timer::default());
5808 info.add_timer(TimerId { id: usize::MAX }, Timer::default());
5809 info.remove_timer(TimerId { id: usize::MAX });
5810 info.remove_thread(ThreadId::unique());
5811
5812 let changes = info.take_changes();
5813 assert_eq!(changes.len(), 4);
5814 assert!(
5815 matches!(&changes[1], CallbackChange::AddTimer { timer_id, .. } if timer_id.id == usize::MAX)
5816 );
5817 assert!(
5818 matches!(&changes[2], CallbackChange::RemoveTimer { timer_id } if timer_id.id == usize::MAX)
5819 );
5820 assert!(matches!(&changes[3], CallbackChange::RemoveThread { .. }));
5821 });
5822 }
5823
5824 #[test]
5829 fn scroll_to_records_position_verbatim_at_numeric_extremes() {
5830 let positions = [
5831 LogicalPosition::new(0.0, 0.0),
5832 LogicalPosition::new(-0.0, -1_000_000.0),
5833 LogicalPosition::new(f32::MIN, f32::MAX),
5834 LogicalPosition::new(f32::INFINITY, f32::NEG_INFINITY),
5835 LogicalPosition::new(f32::NAN, f32::NAN),
5836 ];
5837
5838 with_info(node_none(), |info| {
5839 for p in positions {
5840 info.scroll_to(DomId::ROOT_ID, NodeHierarchyItemId::NONE, p);
5841 }
5842 let changes = info.take_changes();
5843 assert_eq!(changes.len(), positions.len());
5844
5845 for (change, expected) in changes.iter().zip(positions) {
5846 let CallbackChange::ScrollTo {
5847 position, unclamped, ..
5848 } = change
5849 else {
5850 panic!("expected ScrollTo, got {change:?}");
5851 };
5852 assert!(!*unclamped, "scroll_to must request clamping");
5853 assert_eq!(position.x.is_nan(), expected.x.is_nan());
5856 if !expected.x.is_nan() {
5857 assert_eq!(position.x, expected.x);
5858 assert_eq!(position.y, expected.y);
5859 }
5860 }
5861 });
5862 }
5863
5864 #[test]
5865 fn scroll_to_unclamped_sets_the_unclamped_flag() {
5866 with_info(node_none(), |info| {
5867 info.scroll_to_unclamped(
5868 DomId { inner: usize::MAX },
5869 NodeHierarchyItemId::from_raw(usize::MAX),
5870 LogicalPosition::new(-99999.0, 99999.0),
5871 );
5872 let changes = info.take_changes();
5873 assert_eq!(changes.len(), 1);
5874 let CallbackChange::ScrollTo {
5875 unclamped,
5876 dom_id,
5877 position,
5878 ..
5879 } = &changes[0]
5880 else {
5881 panic!("expected ScrollTo");
5882 };
5883 assert!(*unclamped, "scroll_to_unclamped must skip clamping");
5884 assert_eq!(dom_id.inner, usize::MAX, "an unknown DomId is not rejected here");
5885 assert_eq!(position.x, -99999.0);
5886 });
5887 }
5888
5889 #[test]
5890 fn scroll_node_into_view_queues_the_options_verbatim() {
5891 use crate::managers::scroll_into_view::ScrollIntoViewOptions;
5892 with_info(node_none(), |info| {
5893 info.scroll_node_into_view(node_none(), ScrollIntoViewOptions::nearest());
5894 let changes = info.take_changes();
5895 assert_eq!(changes.len(), 1);
5896 assert!(matches!(changes[0], CallbackChange::ScrollIntoView { .. }));
5897 });
5898 }
5899
5900 #[test]
5901 fn open_menu_at_and_show_tooltip_at_accept_extreme_positions() {
5902 let menu = || Menu::create(azul_core::menu::MenuItemVec::from_const_slice(&[]));
5903
5904 with_info(node_none(), |info| {
5905 info.open_menu(menu());
5906 info.open_menu_at(menu(), LogicalPosition::new(0.0, 0.0));
5907 info.open_menu_at(menu(), LogicalPosition::new(f32::MIN, f32::MAX));
5908 info.open_menu_at(menu(), LogicalPosition::new(f32::NAN, f32::INFINITY));
5909
5910 let changes = info.take_changes();
5911 assert_eq!(changes.len(), 4);
5912 assert!(matches!(
5914 &changes[0],
5915 CallbackChange::OpenMenu { position: None, .. }
5916 ));
5917 for change in &changes[1..] {
5919 assert!(matches!(
5920 change,
5921 CallbackChange::OpenMenu {
5922 position: Some(_),
5923 ..
5924 }
5925 ));
5926 }
5927 });
5928
5929 with_info(node_none(), |info| {
5930 info.show_tooltip(AzString::from(""));
5931 info.show_tooltip_at(AzString::from("🌍"), LogicalPosition::new(f32::NAN, -0.0));
5932 info.show_tooltip_at(
5933 AzString::from("x".repeat(100_000)),
5934 LogicalPosition::new(f32::MAX, f32::MIN),
5935 );
5936 let changes = info.take_changes();
5937 assert_eq!(changes.len(), 3);
5938 assert!(matches!(&changes[0], CallbackChange::ShowTooltip { text, .. } if text.as_str().is_empty()));
5939 assert!(matches!(&changes[1], CallbackChange::ShowTooltip { text, position } if text.as_str() == "🌍" && position.x.is_nan()));
5940 assert!(matches!(&changes[2], CallbackChange::ShowTooltip { text, .. } if text.as_str().len() == 100_000));
5941 });
5942 }
5943
5944 #[test]
5949 fn set_css_property_wraps_a_single_property() {
5950 with_info(node_none(), |info| {
5951 info.set_css_property(node0(), a_css_property());
5952 let changes = info.take_changes();
5953 assert_eq!(changes.len(), 1);
5954 let CallbackChange::ChangeNodeCssProperties {
5955 dom_id,
5956 node_id,
5957 properties,
5958 } = &changes[0]
5959 else {
5960 panic!("expected ChangeNodeCssProperties");
5961 };
5962 assert_eq!(*dom_id, DomId::ROOT_ID);
5963 assert_eq!(node_id.index(), 0);
5964 assert_eq!(properties.len(), 1);
5965 });
5966 }
5967
5968 #[test]
5969 fn override_css_property_uses_the_override_channel_not_the_cascade() {
5970 with_info(node_none(), |info| {
5971 info.override_css_property(node0(), a_css_property());
5972 let changes = info.take_changes();
5973 assert_eq!(changes.len(), 1);
5974 assert!(
5975 matches!(changes[0], CallbackChange::OverrideNodeCssProperties { .. }),
5976 "must not fall back to the invalidating ChangeNodeCssProperties path"
5977 );
5978 });
5979 }
5980
5981 #[test]
5982 #[should_panic(expected = "DomNodeId node should not be None")]
5983 fn set_css_property_panics_on_a_none_node_as_documented() {
5984 with_info(node_none(), |info| {
5985 info.set_css_property(node_none(), a_css_property());
5986 });
5987 }
5988
5989 #[test]
5990 #[should_panic(expected = "DomNodeId node should not be None")]
5991 fn override_css_property_panics_on_a_none_node_as_documented() {
5992 with_info(node_none(), |info| {
5993 info.override_css_property(node_none(), a_css_property());
5994 });
5995 }
5996
5997 #[test]
5998 fn change_node_css_properties_accepts_an_empty_property_vec() {
5999 with_info(node_none(), |info| {
6000 info.change_node_css_properties(
6001 DomId::ROOT_ID,
6002 NodeId::new(usize::MAX),
6003 CssPropertyVec::from_const_slice(&[]),
6004 );
6005 let changes = info.take_changes();
6006 assert_eq!(changes.len(), 1);
6007 assert!(
6008 matches!(&changes[0], CallbackChange::ChangeNodeCssProperties { properties, .. } if properties.is_empty())
6009 );
6010 });
6011 }
6012
6013 #[test]
6018 fn change_node_text_passes_hostile_strings_through_unchanged() {
6019 let inputs = [
6020 String::new(),
6021 " \t\n ".to_string(),
6022 "\u{0}embedded nul".to_string(),
6023 "🌍é\u{301}\u{200B}".to_string(),
6024 "x".repeat(1_000_000),
6025 ];
6026
6027 with_info(node_none(), |info| {
6028 for s in &inputs {
6029 info.change_node_text(node0(), AzString::from(s.clone()));
6030 }
6031 let changes = info.take_changes();
6032 assert_eq!(changes.len(), inputs.len());
6033 for (change, expected) in changes.iter().zip(&inputs) {
6034 let CallbackChange::ChangeNodeText { text, .. } = change else {
6035 panic!("expected ChangeNodeText");
6036 };
6037 assert_eq!(text.as_str(), expected.as_str());
6038 }
6039 });
6040 }
6041
6042 #[test]
6043 fn insert_child_node_accepts_empty_and_garbage_type_strings() {
6044 with_info(node_none(), |info| {
6045 info.insert_child_node(
6047 DomId::ROOT_ID,
6048 NodeId::new(0),
6049 AzString::from(""),
6050 OptionUsize::None,
6051 StringVec::from_const_slice(&[]),
6052 OptionString::None,
6053 );
6054 info.insert_child_node(
6055 DomId { inner: usize::MAX },
6056 NodeId::new(usize::MAX),
6057 AzString::from("\u{0}<<not a tag>>"),
6058 OptionUsize::Some(usize::MAX),
6059 StringVec::from_const_slice(&[]),
6060 OptionString::None,
6061 );
6062 assert_eq!(info.take_changes().len(), 2);
6063 });
6064 }
6065
6066 #[test]
6067 fn text_editing_mutators_queue_their_changes() {
6068 with_info(node_none(), |info| {
6069 info.insert_text(DomId::ROOT_ID, NodeId::new(0), AzString::from("🌍"));
6070 info.move_cursor(DomId::ROOT_ID, NodeId::new(0), a_cursor());
6071 info.set_selection(
6072 DomId::ROOT_ID,
6073 NodeId::new(0),
6074 Selection::Cursor(a_cursor()),
6075 );
6076 info.set_text_changeset(PendingTextEdit {
6077 node: node0(),
6078 inserted_text: AzString::from(""),
6079 old_text: AzString::from(""),
6080 });
6081 info.create_text_input(AzString::from("\u{0}"));
6082 info.delete_node(DomId::ROOT_ID, NodeId::new(usize::MAX));
6083 info.set_node_ids_and_classes(
6084 DomId::ROOT_ID,
6085 NodeId::new(0),
6086 azul_core::dom::IdOrClassVec::from_const_slice(&[]),
6087 );
6088
6089 let changes = info.take_changes();
6090 assert_eq!(changes.len(), 7);
6091 assert!(matches!(&changes[0], CallbackChange::InsertText { text, .. } if text.as_str() == "🌍"));
6092 assert!(matches!(changes[1], CallbackChange::MoveCursor { .. }));
6093 assert!(matches!(changes[2], CallbackChange::SetSelection { .. }));
6094 assert!(matches!(changes[3], CallbackChange::SetTextChangeset { .. }));
6095 assert!(matches!(changes[5], CallbackChange::DeleteNode { .. }));
6096 });
6097 }
6098
6099 #[test]
6100 fn image_cache_mutators_accept_empty_ids_and_null_images() {
6101 with_info(node_none(), |info| {
6102 let img = || {
6103 ImageRef::null_image(0, 0, azul_core::resources::RawImageFormat::RGBA8, Vec::new())
6104 };
6105 info.add_image_to_cache(AzString::from(""), img());
6106 info.remove_image_from_cache(AzString::from(""));
6107 info.change_node_image(
6108 DomId::ROOT_ID,
6109 NodeId::new(0),
6110 img(),
6111 UpdateImageType::Content,
6112 );
6113 info.update_image_callback(DomId { inner: usize::MAX }, NodeId::new(usize::MAX));
6114 info.trigger_virtual_view_rerender(DomId::ROOT_ID, NodeId::new(usize::MAX));
6115 assert_eq!(info.take_changes().len(), 5);
6116 });
6117 }
6118
6119 #[test]
6120 fn focus_mutators_queue_set_focus_target() {
6121 with_info(node_none(), |info| {
6122 info.set_focus(FocusTarget::NoFocus);
6123 info.set_focus_to_node(DomId::ROOT_ID, NodeId::new(usize::MAX - 1));
6127 info.focus_next();
6128 info.focus_previous();
6129 info.focus_first();
6130 info.focus_last();
6131 info.clear_focus();
6132
6133 let changes = info.take_changes();
6134 assert_eq!(changes.len(), 7);
6135 for change in &changes {
6136 assert!(matches!(change, CallbackChange::SetFocusTarget { .. }));
6137 }
6138 assert!(matches!(
6139 &changes[2],
6140 CallbackChange::SetFocusTarget {
6141 target: FocusTarget::Next
6142 }
6143 ));
6144 assert!(matches!(
6145 &changes[6],
6146 CallbackChange::SetFocusTarget {
6147 target: FocusTarget::NoFocus
6148 }
6149 ));
6150 });
6151 }
6152
6153 #[test]
6154 fn create_window_queues_window_creation() {
6155 with_info(node_none(), |info| {
6156 info.create_window(WindowCreateOptions::default());
6157 let changes = info.take_changes();
6158 assert_eq!(changes.len(), 1);
6159 assert!(matches!(changes[0], CallbackChange::CreateNewWindow { .. }));
6160 });
6161 }
6162
6163 #[test]
6168 fn route_getters_return_empty_strings_when_no_route_is_active() {
6169 with_info(node_none(), |info| {
6170 assert_eq!(info.get_route_pattern().as_str(), "");
6171 assert_eq!(info.get_route_param(AzString::from("id")).as_str(), "");
6172 assert_eq!(info.get_route_param(AzString::from("")).as_str(), "");
6174 assert_eq!(info.get_route_param(AzString::from("\u{0}🌍")).as_str(), "");
6175 assert_eq!(
6176 info.get_route_param(AzString::from("k".repeat(100_000)))
6177 .as_str(),
6178 ""
6179 );
6180 });
6181 }
6182
6183 #[test]
6184 fn set_route_param_without_an_active_route_queues_nothing() {
6185 with_info(node_none(), |info| {
6186 info.set_route_param(AzString::from("id"), AzString::from("42"));
6187 assert!(
6188 info.take_changes().is_empty(),
6189 "no active route => no SwitchRoute change may be queued"
6190 );
6191 });
6192 }
6193
6194 #[test]
6195 fn switch_route_queues_the_pattern_verbatim() {
6196 with_info(node_none(), |info| {
6197 info.switch_route(
6198 AzString::from("/user/:id"),
6199 azul_core::window::StringPairVec::from_vec(vec![azul_core::window::AzStringPair {
6200 key: AzString::from("id"),
6201 value: AzString::from("42"),
6202 }]),
6203 );
6204 let changes = info.take_changes();
6205 assert_eq!(changes.len(), 1);
6206 assert!(
6207 matches!(&changes[0], CallbackChange::SwitchRoute { pattern, params } if pattern.as_str() == "/user/:id" && params.len() == 1)
6208 );
6209 });
6210 }
6211
6212 #[test]
6217 fn get_node_id_by_id_attribute_returns_none_for_hostile_ids() {
6218 let long = "a".repeat(1_000_000);
6219 let nested = "[".repeat(10_000);
6220 let ids: [&str; 12] = [
6221 "",
6222 " ",
6223 "\t\n",
6224 "\u{0}",
6225 "!@#$%^&*()",
6226 "0",
6227 "-0",
6228 "9223372036854775807",
6229 "NaN",
6230 "inf",
6231 " valid ",
6232 "valid;garbage",
6233 ];
6234
6235 with_info(node_none(), |info| {
6236 for id in ids {
6237 assert_eq!(
6238 info.get_node_id_by_id_attribute(DomId::ROOT_ID, id),
6239 None,
6240 "id {id:?} must not resolve in an empty layout tree"
6241 );
6242 }
6243 for id in ["\u{1F600}", "e\u{301}", "🌍🌍🌍"] {
6245 assert_eq!(info.get_node_id_by_id_attribute(DomId::ROOT_ID, id), None);
6246 }
6247 assert_eq!(
6249 info.get_node_id_by_id_attribute(DomId::ROOT_ID, &long),
6250 None
6251 );
6252 assert_eq!(
6253 info.get_node_id_by_id_attribute(DomId::ROOT_ID, &nested),
6254 None
6255 );
6256 assert_eq!(
6258 info.get_node_id_by_id_attribute(DomId { inner: usize::MAX }, "x"),
6259 None
6260 );
6261 });
6262 }
6263
6264 #[test]
6265 fn hierarchy_navigation_is_none_and_zero_on_an_empty_layout_tree() {
6266 with_info(node_none(), |info| {
6267 for dom in [DomId::ROOT_ID, DomId { inner: usize::MAX }] {
6268 for node in [NodeId::new(0), NodeId::new(usize::MAX)] {
6269 assert_eq!(info.get_parent_node(dom, node), None);
6270 assert_eq!(info.get_next_sibling_node(dom, node), None);
6271 assert_eq!(info.get_previous_sibling_node(dom, node), None);
6272 assert_eq!(info.get_first_child_node(dom, node), None);
6273 assert_eq!(info.get_last_child_node(dom, node), None);
6274 assert_eq!(info.get_children_count(dom, node), 0);
6275 assert_eq!(info.get_all_children_nodes(dom, node).len(), 0);
6276 }
6277 }
6278 assert_eq!(info.get_parent(node0()), None);
6280 assert_eq!(info.get_first_child(node0()), None);
6281 assert_eq!(info.get_last_child(node0()), None);
6282 assert_eq!(info.get_next_sibling(node_none()), None);
6283 assert_eq!(info.get_previous_sibling(node_none()), None);
6284 });
6285 }
6286
6287 #[test]
6288 fn geometry_and_css_queries_are_none_on_an_empty_layout_tree() {
6289 with_info(node0(), |info| {
6290 assert_eq!(info.get_node_size(node0()), None);
6291 assert_eq!(info.get_node_position(node0()), None);
6292 assert_eq!(info.get_node_rect(node0()), None);
6293 assert_eq!(info.get_node_hit_test_bounds(node0()), None);
6294 assert_eq!(info.get_hit_node_rect(), None);
6295 assert!(info.get_computed_width(node0()).is_none());
6296 assert!(info.get_computed_height(node0()).is_none());
6297 assert!(info
6298 .get_computed_css_property(node_none(), CssPropertyType::Width)
6299 .is_none());
6300 assert!(info.get_layout_result(&DomId::ROOT_ID).is_none());
6301 assert!(info.get_gpu_cache(&DomId::ROOT_ID).is_none());
6302 assert_eq!(info.get_dom_ids().len(), 0);
6303 });
6304 }
6305
6306 #[test]
6307 fn state_getters_reflect_the_construction_arguments() {
6308 let hit = node0();
6309 with_info(hit, |info| {
6310 assert_eq!(info.get_hit_node(), hit);
6311 assert!(info.get_cursor_relative_to_viewport().is_none());
6313 assert!(info.get_cursor_relative_to_node().is_none());
6314 assert!(info.get_ctx().is_none());
6316 assert!(info.get_gl_context().is_none());
6317 assert!(info.get_previous_window_state().is_none());
6319 assert!(info.get_previous_window_flags().is_none());
6320 assert!(info.get_previous_mouse_state().is_none());
6321 assert!(info.get_previous_keyboard_state().is_none());
6322 assert!(matches!(
6323 info.get_current_window_handle(),
6324 RawWindowHandle::Unsupported
6325 ));
6326 assert_eq!(info.get_monitors().len(), 0);
6327 assert!(info.get_current_monitor().is_none());
6328 assert_eq!(info.get_timer_ids().len(), 0);
6329 assert_eq!(info.get_thread_ids().len(), 0);
6330 assert!(info.get_timer(&TimerId { id: 0 }).is_none());
6331 assert!(info.get_thread(&ThreadId::unique()).is_none());
6332 let _now = info.get_current_time();
6334 });
6335 }
6336
6337 #[test]
6338 fn selection_and_undo_queries_are_empty_for_unknown_nodes() {
6339 with_info(node_none(), |info| {
6340 assert!(!info.has_any_selection());
6341 assert_eq!(info.get_selection_count(&DomId::ROOT_ID), 0);
6342 assert!(info.get_primary_selection(&DomId::ROOT_ID).is_none());
6343 assert!(!info.node_has_selection(node0()));
6344
6345 for node in [NodeId::new(0), NodeId::new(usize::MAX)] {
6346 assert!(!info.can_undo(node));
6347 assert!(!info.can_redo(node));
6348 assert!(info.get_undo_text(node).is_none());
6349 assert!(info.get_redo_text(node).is_none());
6350 assert!(info.inspect_undo_operation(node).is_none());
6351 assert!(info.inspect_redo_operation(node).is_none());
6352 }
6353
6354 assert!(info.get_node_text_content(node0()).is_none());
6355 assert_eq!(info.get_node_text_length(node0()), None);
6356 assert!(info.get_text_changeset().is_none());
6357 assert!(!info.is_node_focused(node0()));
6358 assert!(!info.has_focus(node0()));
6359 assert!(info.get_focused_node().is_none());
6360 });
6361 }
6362
6363 #[test]
6364 fn cursor_inspection_is_none_without_a_text_layout() {
6365 with_info(node_none(), |info| {
6366 assert!(info.inspect_move_cursor_left(node0()).is_none());
6367 assert!(info.inspect_move_cursor_right(node0()).is_none());
6368 assert!(info.inspect_move_cursor_up(node0()).is_none());
6369 assert!(info.inspect_move_cursor_down(node0()).is_none());
6370 assert!(info.inspect_move_cursor_to_line_start(node0()).is_none());
6371 assert!(info.inspect_move_cursor_to_line_end(node0()).is_none());
6372 assert!(info.inspect_backspace(node0()).is_none());
6373 assert!(info.inspect_delete(node0()).is_none());
6374 assert!(info.inspect_move_cursor_left(node_none()).is_none());
6376 assert!(info.inspect_backspace(node_none()).is_none());
6377 });
6378 }
6379
6380 #[test]
6381 fn drag_and_gesture_queries_are_inactive_by_default() {
6382 with_info(node_none(), |info| {
6383 assert!(!info.is_dragging());
6384 assert!(!info.is_drag_active());
6385 assert!(!info.is_node_drag_active());
6386 assert!(!info.is_file_drag_active());
6387 assert!(info.get_drag_delta().is_none());
6388 assert!(info.get_drag_delta_screen().is_none());
6389 assert!(info.get_drag_delta_screen_incremental().is_none());
6390 assert!(!info.was_double_clicked());
6391 assert!(info.get_pen_pressure().is_none());
6392 assert!(info.get_pen_tilt().is_none());
6393 assert!(!info.is_pen_in_contact());
6394 assert!(!info.is_pen_eraser());
6395 assert!(!info.is_pen_barrel_button_pressed());
6396 assert_eq!(info.get_drag_types().len(), 0);
6397 assert!(info.get_drag_data("text/plain").is_none());
6398 assert!(info.get_drag_data("").is_none());
6399 });
6400 }
6401
6402 #[test]
6403 #[cfg(feature = "text_layout")]
6404 fn get_loaded_font_bytes_returns_none_for_boundary_hashes() {
6405 with_info(node_none(), |info| {
6406 for hash in [0u64, 1, u64::MAX, u64::MAX / 2] {
6409 assert!(info.get_loaded_font_bytes(hash).is_none());
6410 }
6411 assert_eq!(info.get_loaded_fonts().len(), 0);
6412 });
6413 }
6414
6415 #[test]
6416 #[cfg(feature = "cpurender")]
6417 fn take_screenshot_of_a_missing_dom_is_an_error_not_a_panic() {
6418 with_info(node_none(), |info| {
6419 let err = info
6420 .take_screenshot(DomId::ROOT_ID)
6421 .expect_err("an empty layout window has no DOM to screenshot");
6422 assert_eq!(err.as_str(), "DOM not found in layout results");
6423
6424 let err = info
6425 .take_screenshot(DomId { inner: usize::MAX })
6426 .expect_err("an out-of-range DomId must be rejected");
6427 assert_eq!(err.as_str(), "DOM not found in layout results");
6428
6429 assert!(info.take_screenshot_base64(DomId::ROOT_ID).is_err());
6430 });
6431 }
6432
6433 #[test]
6438 fn callback_change_is_debug_and_clone() {
6439 let change = CallbackChange::ScrollTo {
6440 dom_id: DomId::ROOT_ID,
6441 node_id: NodeHierarchyItemId::NONE,
6442 position: LogicalPosition::new(f32::NAN, 0.0),
6443 unclamped: true,
6444 };
6445 let cloned = change.clone();
6446 assert!(matches!(
6447 cloned,
6448 CallbackChange::ScrollTo { unclamped: true, .. }
6449 ));
6450 assert!(!format!("{change:?}").is_empty());
6451 }
6452}