1use std::{
22 collections::{BTreeMap, BTreeSet, HashMap},
23 sync::{
24 atomic::{AtomicUsize, Ordering},
25 Arc,
26 },
27};
28
29use azul_core::{
30 resources::UpdateImageType,
31 callbacks::{FocusTarget, HidpiAdjustedBounds, VirtualViewCallbackReason, Update},
32 dom::{
33 AccessibilityAction, AttributeType, Dom, DomId, DomIdVec, DomNodeId, NodeId, NodeType, On,
34 },
35 events::{EasingFunction, EventFilter, FocusEventFilter, HoverEventFilter},
36 geom::{LogicalPosition, LogicalRect, LogicalSize, OptionLogicalPosition},
37 gl::OptionGlContextPtr,
38 gpu::{GpuScrollbarOpacityEvent, GpuValueCache},
39 hit_test::{DocumentId, ScrollPosition, ScrollbarHitId},
40 refany::{OptionRefAny, RefAny},
41 resources::{
42 Epoch, FontKey, GlTextureCache, IdNamespace, ImageCache, ImageMask, ImageRef, ImageRefHash,
43 OpacityKey, RendererResources,
44 },
45 selection::{
46 CursorAffinity, GraphemeClusterId, Selection, SelectionAnchor, SelectionFocus,
47 SelectionRange, SelectionState, TextCursor, TextSelection,
48 },
49 styled_dom::{
50 collect_nodes_in_document_order, is_before_in_document_order, NodeHierarchyItemId,
51 StyledDom,
52 },
53 task::{
54 Duration, Instant, SystemTickDiff, SystemTimeDiff, TerminateTimer, ThreadId, ThreadIdVec,
55 ThreadSendMsg, TimerId, TimerIdVec,
56 },
57 window::{CursorPosition, MonitorVec, RawWindowHandle, RendererType},
58 FastBTreeSet, OrderedMap,
59};
60use azul_css::{
61 css::Css,
62 props::{
63 basic::FontRef,
64 property::{CssProperty, CssPropertyVec},
65 },
66 AzString, LayoutDebugMessage, OptionString,
67};
68use rust_fontconfig::FcFontCache;
69
70#[cfg(feature = "icu")]
71use crate::icu::IcuLocalizerHandle;
72use crate::{
73 callbacks::{
74 Callback, ExternalSystemCallbacks, MenuCallback,
75 },
76 managers::{
77 gpu_state::GpuStateManager,
78 virtual_view::VirtualViewManager,
79 scroll_state::ScrollManager,
80 },
81 solver3::{
82 self, cache::LayoutCache as Solver3LayoutCache, display_list::DisplayList,
83 layout_tree::LayoutTree,
84 },
85 text3::{
86 cache::{
87 FontManager, FontSelector, FontStyle, InlineContent, TextShapingCache as TextLayoutCache,
88 LayoutError, ShapedItem, StyleProperties, StyledRun, UnifiedConstraints,
89 UnifiedLayout,
90 },
91 default::PathLoader,
92 },
93 thread::{OptionThreadReceiveMsg, Thread, ThreadReceiveMsg, ThreadWriteBackMsg},
94 timer::Timer,
95 window_state::{FullWindowState, WindowCreateOptions},
96};
97
98static DOCUMENT_ID_COUNTER: AtomicUsize = AtomicUsize::new(0);
100static ID_NAMESPACE_COUNTER: AtomicUsize = AtomicUsize::new(0);
101
102#[allow(clippy::cast_possible_truncation)] fn new_document_id() -> DocumentId {
105 let namespace_id = new_id_namespace();
106 let id = DOCUMENT_ID_COUNTER.fetch_add(1, Ordering::Relaxed) as u32;
107 DocumentId { namespace_id, id }
108}
109
110#[derive(Debug, Clone)]
115#[allow(clippy::large_enum_variant)]
118pub enum CursorBlinkTimerAction {
119 Start(Timer),
121 Stop,
123 NoChange,
125}
126
127#[derive(Debug, Clone)]
131#[allow(clippy::large_enum_variant)]
134pub enum TooltipTimerAction {
135 Start(Timer),
137 Stop,
139 NoChange,
141}
142
143#[allow(clippy::cast_possible_truncation)] fn new_id_namespace() -> IdNamespace {
146 let id = ID_NAMESPACE_COUNTER.fetch_add(1, Ordering::Relaxed) as u32;
147 IdNamespace(id)
148}
149
150#[cfg(feature = "std")]
154extern "C" fn virtual_view_measure_dom_trampoline(
155 ctx: *mut core::ffi::c_void,
156 dom: *mut Dom,
157 available: LogicalSize,
158) -> LogicalSize {
159 if ctx.is_null() || dom.is_null() {
160 return LogicalSize::zero();
161 }
162 let lw = unsafe { &*(ctx as *const LayoutWindow) };
166 let dom = unsafe { core::ptr::read(dom) };
167 lw.measure_dom(dom, available)
168}
169
170extern "C" fn cursor_blink_timer_destructor(_: RefAny) {
176 }
178
179#[must_use] pub extern "C" fn cursor_blink_timer_callback(
189 _data: RefAny,
190 mut info: crate::timer::TimerCallbackInfo,
191) -> azul_core::callbacks::TimerCallbackReturn {
192 use azul_core::callbacks::{TimerCallbackReturn, Update};
193 use azul_core::task::TerminateTimer;
194
195 let now = info.get_current_time();
197
198 info.set_cursor_visibility_toggle();
216
217 TimerCallbackReturn {
225 should_update: Update::DoNothing,
226 should_terminate: TerminateTimer::Continue,
227 }
228}
229
230#[must_use] pub extern "C" fn tooltip_delay_timer_callback(
244 _data: RefAny,
245 mut info: crate::timer::TimerCallbackInfo,
246) -> azul_core::callbacks::TimerCallbackReturn {
247 use azul_core::callbacks::{TimerCallbackReturn, Update};
248 use azul_core::task::TerminateTimer;
249
250 let layout_window = info.callback_info.get_layout_window();
251 let hover_node_id = layout_window
252 .hover_manager
253 .current_hover_node()
254 .map(|node_id| DomNodeId {
255 dom: DomId { inner: 0 },
256 node: NodeHierarchyItemId::from_crate_internal(Some(node_id)),
257 });
258
259 if let Some(dom_node_id) = hover_node_id {
260 let tooltip_text = info
262 .callback_info
263 .get_node_attribute(dom_node_id, "aria-label")
264 .or_else(|| info.callback_info.get_node_attribute(dom_node_id, "alt"))
265 .or_else(|| info.callback_info.get_node_attribute(dom_node_id, "title"));
266
267 if let Some(text) = tooltip_text {
268 info.callback_info.show_tooltip(text);
269 }
270 }
271
272 TimerCallbackReturn {
273 should_update: Update::DoNothing,
274 should_terminate: TerminateTimer::Terminate,
275 }
276}
277
278#[derive(Debug, Clone, Default, PartialEq, Eq)]
286pub enum FrameDamage {
287 #[default]
289 None,
290 Rects(Vec<LogicalRect>),
292 Full,
294}
295
296impl FrameDamage {
297 #[must_use]
299 pub const fn is_none(&self) -> bool {
300 matches!(self, Self::None)
301 }
302
303 #[must_use]
305 pub const fn is_full(&self) -> bool {
306 matches!(self, Self::Full)
307 }
308
309 #[must_use]
311 pub const fn rect_count(&self) -> usize {
312 match self {
313 Self::None => 0,
314 Self::Full => 1,
315 Self::Rects(r) => r.len(),
316 }
317 }
318
319 #[must_use]
321 pub fn rects(&self) -> Option<&[LogicalRect]> {
322 match self {
323 Self::Rects(r) => Some(r),
324 _ => None,
325 }
326 }
327
328 #[must_use]
331 pub fn area(&self, window_area: f32) -> f32 {
332 match self {
333 Self::None => 0.0,
334 Self::Full => window_area,
335 Self::Rects(r) => r.iter().map(|r| r.size.width * r.size.height).sum(),
336 }
337 }
338
339 #[must_use]
359 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
360 pub fn to_present_rects_physical(
361 &self,
362 dpi_factor: f32,
363 buf_w: u32,
364 buf_h: u32,
365 force_full: bool,
366 ) -> Option<Vec<(u32, u32, u32, u32)>> {
367 const MAX_PRESENT_RECTS: usize = 16;
368 if buf_w == 0 || buf_h == 0 {
369 return None;
370 }
371 let full = || Some(vec![(0u32, 0u32, buf_w, buf_h)]);
372 if force_full {
373 return full();
377 }
378 match self {
379 Self::None => None,
380 Self::Full => full(),
381 Self::Rects(rects) => {
382 if rects.is_empty() {
383 return None;
384 }
385 if rects.len() > MAX_PRESENT_RECTS {
386 return full();
387 }
388 let mut out = Vec::with_capacity(rects.len());
389 for r in rects {
390 let x0 = ((r.origin.x * dpi_factor).floor() as i64).clamp(0, i64::from(buf_w));
391 let y0 = ((r.origin.y * dpi_factor).floor() as i64).clamp(0, i64::from(buf_h));
392 let x1 = (((r.origin.x + r.size.width) * dpi_factor).ceil() as i64)
393 .clamp(0, i64::from(buf_w));
394 let y1 = (((r.origin.y + r.size.height) * dpi_factor).ceil() as i64)
395 .clamp(0, i64::from(buf_h));
396 if x1 > x0 && y1 > y0 {
397 out.push((x0 as u32, y0 as u32, (x1 - x0) as u32, (y1 - y0) as u32));
398 }
399 }
400 if out.is_empty() {
401 None
402 } else {
403 Some(out)
404 }
405 }
406 }
407 }
408}
409
410#[derive(Debug, Clone, Default, PartialEq, Eq)]
421pub struct FrameReport {
422 pub frame_index: u64,
424 pub paint_damage: FrameDamage,
426 pub present_damage: FrameDamage,
429 pub accumulated_paint_damage: FrameDamage,
436 pub accumulated_present_damage: FrameDamage,
438 pub frames_since_reset: u32,
440 pub reset_generation: u64,
442 pub relayout_iterations: u32,
456 pub dom_regenerations: u32,
459 pub layout_passes: u32,
481 pub hit_depth_cap: bool,
485 pub terminal_result: u8,
487}
488
489pub const MAX_EVENT_RECURSION_DEPTH: usize = 7;
499
500pub const MAX_LIFECYCLE_REGEN_PASSES: usize = 3;
516
517impl FrameReport {
518 pub fn sync_generation_to(&mut self, requested_generation: u64) {
523 if requested_generation != self.reset_generation {
524 self.reset_generation = requested_generation;
525 self.reset_counters();
526 }
527 }
528
529 #[must_use]
540 pub fn as_of_generation(&self, requested_generation: u64) -> Self {
541 let mut out = self.clone();
542 out.sync_generation_to(requested_generation);
543 out
544 }
545
546 pub fn reset_counters(&mut self) {
548 self.relayout_iterations = 0;
549 self.dom_regenerations = 0;
550 self.layout_passes = 0;
551 self.hit_depth_cap = false;
552 self.frames_since_reset = 0;
553 self.accumulated_paint_damage = FrameDamage::None;
554 self.accumulated_present_damage = FrameDamage::None;
555 }
556
557 pub fn record_frame_at_generation(
563 &mut self,
564 requested_generation: u64,
565 paint: FrameDamage,
566 present: FrameDamage,
567 ) {
568 self.sync_generation_to(requested_generation);
569 self.frame_index = self.frame_index.wrapping_add(1);
570 self.frames_since_reset = self.frames_since_reset.saturating_add(1);
571 Self::merge_into(&mut self.accumulated_paint_damage, &paint);
572 Self::merge_into(&mut self.accumulated_present_damage, &present);
573 self.paint_damage = paint;
574 self.present_damage = present;
575 }
576
577 fn merge_into(acc: &mut FrameDamage, next: &FrameDamage) {
578 match (&mut *acc, next) {
579 (_, FrameDamage::None) | (FrameDamage::Full, _) => {}
580 (_, FrameDamage::Full) => *acc = FrameDamage::Full,
581 (FrameDamage::None, FrameDamage::Rects(r)) => *acc = FrameDamage::Rects(r.clone()),
582 (FrameDamage::Rects(a), FrameDamage::Rects(b)) => a.extend(b.iter().copied()),
583 }
584 }
585}
586
587#[derive(Debug)]
589pub struct DomLayoutResult {
590 pub styled_dom: StyledDom,
592 pub layout_tree: LayoutTree,
594 pub calculated_positions: solver3::PositionVec,
596 pub viewport: LogicalRect,
598 pub display_list: DisplayList,
600 pub scroll_ids: HashMap<usize, u64>,
603 pub scroll_id_to_node_id: HashMap<u64, NodeId>,
606}
607
608#[derive(Copy, Debug, Clone)]
610pub struct ScrollbarDragState {
611 pub hit_id: ScrollbarHitId,
612 pub initial_mouse_pos: LogicalPosition,
613 pub initial_scroll_offset: LogicalPosition,
614}
615
616pub use crate::managers::text_input::PendingTextEdit;
620
621#[derive(Debug, Clone)]
624#[derive(Default)]
625pub struct TextConstraintsCache {
626 pub constraints: BTreeMap<(DomId, NodeId), UnifiedConstraints>,
628}
629
630
631#[derive(Debug, Clone)]
634pub struct DirtyTextNode {
635 pub content: Vec<InlineContent>,
637 pub cursor: Option<TextCursor>,
639 pub needs_ancestor_relayout: bool,
641}
642
643#[derive(Debug)]
645pub struct TextChangesetResult {
646 pub dirty_nodes: Vec<DomNodeId>,
648 pub needs_relayout: bool,
651}
652
653#[derive(Debug, Default, Clone)]
668pub struct E2eMountOverride {
669 xml: Option<String>,
670 dirty: bool,
671}
672
673impl E2eMountOverride {
674 pub fn set(&mut self, xml: Option<String>) {
676 self.xml = xml;
677 self.dirty = true;
678 }
679
680 #[must_use]
682 pub fn xml(&self) -> Option<&str> {
683 self.xml.as_deref()
684 }
685
686 #[must_use]
688 pub const fn is_dirty(&self) -> bool {
689 self.dirty
690 }
691
692 pub const fn take_dirty(&mut self) -> bool {
694 core::mem::replace(&mut self.dirty, false)
695 }
696}
697
698#[derive(Debug)]
707pub struct LayoutWindow {
708 pub e2e_mount: E2eMountOverride,
711 #[cfg(feature = "e2e-server")]
717 pub e2e_scratch: std::sync::Mutex<crate::e2e::E2eScratch>,
718 pub skip_gpu_sync: bool,
726 pub frame_report: FrameReport,
730 pub frame_report_reset_request: core::sync::atomic::AtomicU64,
741 #[cfg(feature = "pdf")]
743 pub fragmentation_context: crate::paged::FragmentationContext,
744 pub layout_cache: Solver3LayoutCache,
746 pub text_cache: TextLayoutCache,
748 pub font_manager: FontManager<FontRef>,
750 pub image_cache: ImageCache,
752 pub cpu_image_callback_results: BTreeMap<ImageRefHash, ImageRef>,
759 pub layout_results: BTreeMap<DomId, DomLayoutResult>,
761 pub scroll_manager: ScrollManager,
763 pub gesture_drag_manager: crate::managers::gesture::GestureAndDragManager,
765 pub focus_manager: crate::managers::focus_cursor::FocusManager,
767 pub text_edit_manager: crate::managers::text_edit::TextEditManager,
769 pub file_drop_manager: crate::managers::file_drop::FileDropManager,
771 pub clipboard_manager: crate::managers::clipboard::ClipboardManager,
773 pub hover_manager: crate::managers::hover::HoverManager,
775 pub virtual_view_manager: VirtualViewManager,
777 pub gpu_state_manager: GpuStateManager,
779 pub a11y_manager: crate::managers::a11y::A11yManager,
781 pub permission_manager: crate::managers::permission::PermissionManager,
788 pub geolocation_manager: crate::managers::geolocation::GeolocationManager,
795 pub biometric_manager: crate::managers::biometric::BiometricManager,
800 pub keyring_manager: crate::managers::keyring::KeyringManager,
805 pub sensor_manager: crate::managers::sensors::SensorManager,
810 pub gamepad_manager: crate::managers::gamepad::GamepadManager,
814 pub safe_area_insets: azul_css::system::SafeAreaInsets,
818 pub timers: BTreeMap<TimerId, Timer>,
820 pub threads: BTreeMap<ThreadId, Thread>,
822 pub renderer_resources: RendererResources,
824 pub renderer_type: Option<RendererType>,
826 pub previous_window_state: Option<FullWindowState>,
828 pub current_window_state: FullWindowState,
831 pub document_id: DocumentId,
834 pub id_namespace: IdNamespace,
836 pub epoch: Epoch,
839 pub gl_texture_cache: GlTextureCache,
841 currently_dragging_thumb: Option<ScrollbarDragState>,
843 pub text_input_manager: crate::managers::text_input::TextInputManager,
845 pub undo_redo_manager: crate::managers::undo_redo::UndoRedoManager,
847 pub text_constraints_cache: TextConstraintsCache,
850 pub dirty_text_nodes: BTreeMap<(DomId, NodeId), DirtyTextNode>,
854 pub pending_virtual_view_updates: BTreeMap<DomId, BTreeMap<NodeId, VirtualViewCallbackReason>>,
860 pub pending_lifecycle_events: Vec<azul_core::events::SyntheticEvent>,
870 pub pending_unmount_invocations: Vec<(
879 azul_core::callbacks::CoreCallbackData,
880 azul_core::events::SyntheticEvent,
881 )>,
882 pub system_style: Option<Arc<azul_css::system::SystemStyle>>,
885 pub monitors: Arc<std::sync::Mutex<MonitorVec>>,
889 font_stacks_hash: u64,
893 pre_preedit_content: Option<Vec<InlineContent>>,
897 pub input_interpreter: azul_core::events::InputInterpreterCallback,
901 pub post_filter: azul_core::events::PostFilterCallback,
904 pub routes: azul_core::resources::RouteVec,
907 #[cfg(feature = "icu")]
910 pub icu_localizer: IcuLocalizerHandle,
911}
912
913const fn default_duration_500ms() -> Duration {
914 Duration::System(SystemTimeDiff::from_millis(500))
915}
916
917const fn default_duration_200ms() -> Duration {
918 Duration::System(SystemTimeDiff::from_millis(200))
919}
920
921const fn duration_to_millis(duration: Duration) -> u64 {
928 duration.as_millis_u64()
929}
930
931impl LayoutWindow {
932 pub fn request_frame_report_reset(&self) {
938 self.frame_report_reset_request
939 .fetch_add(1, Ordering::SeqCst);
940 }
941
942 pub fn sync_frame_report(&mut self) {
945 let generation = self.frame_report_reset_request.load(Ordering::SeqCst);
946 self.frame_report.sync_generation_to(generation);
947 }
948
949 #[must_use]
954 pub fn frame_report_synced(&self) -> FrameReport {
955 let generation = self.frame_report_reset_request.load(Ordering::SeqCst);
956 self.frame_report.as_of_generation(generation)
957 }
958
959 pub fn record_frame(&mut self, paint: FrameDamage, present: FrameDamage) {
962 let generation = self.frame_report_reset_request.load(Ordering::SeqCst);
963 self.frame_report
964 .record_frame_at_generation(generation, paint, present);
965 }
966
967 fn from_font_manager(font_manager: FontManager<FontRef>) -> Self {
974 Self {
975 e2e_mount: E2eMountOverride::default(),
976 #[cfg(feature = "e2e-server")]
977 e2e_scratch: std::sync::Mutex::new(crate::e2e::E2eScratch::default()),
978 skip_gpu_sync: false,
980 frame_report: FrameReport::default(),
981 frame_report_reset_request: core::sync::atomic::AtomicU64::new(0),
982 #[cfg(feature = "pdf")]
983 fragmentation_context: crate::paged::FragmentationContext::new_continuous(800.0),
984 layout_cache: Solver3LayoutCache {
985 tree: None,
986 calculated_positions: Vec::new(),
987 viewport: None,
988 scroll_ids: HashMap::new(),
989 scroll_id_to_node_id: HashMap::new(),
990 counters: HashMap::new(),
991 float_cache: HashMap::new(),
992 cache_map: solver3::cache::LayoutCacheMap::default(),
993 previous_positions: Vec::new(),
994 cached_display_list: None,
995 prev_dom_ptr: 0,
996 prev_viewport: LogicalRect::zero(),
997 },
998 text_cache: TextLayoutCache::new(),
999 font_manager,
1000 image_cache: ImageCache::default(),
1001 cpu_image_callback_results: BTreeMap::new(),
1002 layout_results: BTreeMap::new(),
1003 scroll_manager: ScrollManager::new(),
1004 gesture_drag_manager: crate::managers::gesture::GestureAndDragManager::new(),
1005 focus_manager: crate::managers::focus_cursor::FocusManager::new(),
1006 text_edit_manager: crate::managers::text_edit::TextEditManager::new(),
1007 file_drop_manager: crate::managers::file_drop::FileDropManager::new(),
1008 clipboard_manager: crate::managers::clipboard::ClipboardManager::new(),
1009 hover_manager: crate::managers::hover::HoverManager::new(),
1010 virtual_view_manager: VirtualViewManager::new(),
1011 gpu_state_manager: GpuStateManager::new(
1012 default_duration_500ms(),
1013 default_duration_200ms(),
1014 ),
1015 a11y_manager: crate::managers::a11y::A11yManager::new(),
1016 permission_manager: crate::managers::permission::PermissionManager::new(),
1017 geolocation_manager: crate::managers::geolocation::GeolocationManager::new(),
1018 biometric_manager: crate::managers::biometric::BiometricManager::new(),
1019 keyring_manager: crate::managers::keyring::KeyringManager::new(),
1020 sensor_manager: crate::managers::sensors::SensorManager::new(),
1021 gamepad_manager: crate::managers::gamepad::GamepadManager::new(),
1022 safe_area_insets: azul_css::system::SafeAreaInsets::default(),
1023 timers: BTreeMap::new(),
1024 threads: BTreeMap::new(),
1025 renderer_resources: RendererResources::default(),
1026 renderer_type: None,
1027 previous_window_state: None,
1028 current_window_state: FullWindowState::default(),
1029 document_id: new_document_id(),
1030 id_namespace: new_id_namespace(),
1031 epoch: Epoch::new(),
1032 gl_texture_cache: GlTextureCache::default(),
1033 currently_dragging_thumb: None,
1034 text_input_manager: crate::managers::text_input::TextInputManager::new(),
1035 undo_redo_manager: crate::managers::undo_redo::UndoRedoManager::new(),
1036 text_constraints_cache: TextConstraintsCache {
1037 constraints: BTreeMap::new(),
1038 },
1039 dirty_text_nodes: BTreeMap::new(),
1040 pending_virtual_view_updates: BTreeMap::new(),
1041 pending_lifecycle_events: Vec::new(),
1042 pending_unmount_invocations: Vec::new(),
1043 system_style: None,
1044 monitors: Arc::new(std::sync::Mutex::new(MonitorVec::from_const_slice(&[]))),
1045 font_stacks_hash: 0,
1046 pre_preedit_content: None,
1047 input_interpreter: azul_core::events::InputInterpreterCallback::default(),
1048 post_filter: azul_core::events::PostFilterCallback::default(),
1049 routes: azul_core::resources::RouteVec::from_const_slice(&[]),
1050 #[cfg(feature = "icu")]
1051 icu_localizer: IcuLocalizerHandle::default(),
1052 }
1053 }
1054
1055 pub fn new(fc_cache: FcFontCache) -> Result<Self, solver3::LayoutError> {
1062 Ok(Self::from_font_manager(FontManager::new(fc_cache)?))
1063 }
1064
1065 pub fn from_font_context(ctx: &crate::text3::cache::FontContext) -> Result<Self, solver3::LayoutError> {
1072 let fm = ctx.to_font_manager();
1073 let fc_cache = fm.fc_cache.clone();
1074 let parsed_fonts = fm.parsed_fonts.clone();
1075 let mut lw = Self::new_with_shared_fonts(fc_cache, parsed_fonts)?;
1076 lw.font_manager = fm;
1077 Ok(lw)
1078 }
1079
1080 pub fn new_with_shared_fonts(
1085 fc_cache: FcFontCache,
1086 parsed_fonts: Arc<std::sync::Mutex<HashMap<rust_fontconfig::FontId, FontRef>>>,
1087 ) -> Result<Self, solver3::LayoutError> {
1088 Ok(Self::from_font_manager(FontManager::from_arc_shared(
1089 fc_cache,
1090 parsed_fonts,
1091 )?))
1092 }
1093
1094 #[cfg(feature = "pdf")]
1107 pub fn new_paged(
1108 fc_cache: FcFontCache,
1109 page_size: LogicalSize,
1110 ) -> Result<Self, crate::solver3::LayoutError> {
1111 let mut lw = Self::from_font_manager(FontManager::new(fc_cache)?);
1112 lw.fragmentation_context = crate::paged::FragmentationContext::new_paged(page_size);
1113 Ok(lw)
1114 }
1115
1116 pub fn layout_and_generate_display_list(
1137 &mut self,
1138 root_dom: StyledDom,
1139 window_state: &FullWindowState,
1140 renderer_resources: &RendererResources,
1141 system_callbacks: &ExternalSystemCallbacks,
1142 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1143 ) -> Result<(), solver3::LayoutError> {
1144 self.sync_frame_report();
1151 self.frame_report.layout_passes = self.frame_report.layout_passes.saturating_add(1);
1152
1153 self.layout_results.clear();
1155
1156 self.virtual_view_manager.reset_all_invocation_flags();
1161
1162 if let Some(msgs) = debug_messages.as_mut() {
1163 msgs.push(LayoutDebugMessage::info(format!(
1164 "[layout_and_generate_display_list] Starting layout for DOM with {} nodes",
1165 root_dom.node_data.len()
1166 )));
1167 }
1168
1169 let result = self.layout_dom_recursive(
1172 root_dom,
1173 window_state,
1174 renderer_resources,
1175 system_callbacks,
1176 debug_messages,
1177 );
1178
1179 if let Err(ref e) = result {
1180 if let Some(msgs) = debug_messages.as_mut() {
1181 msgs.push(LayoutDebugMessage::error(format!(
1182 "[layout_and_generate_display_list] Layout FAILED: {e:?}"
1183 )));
1184 }
1185 } else if let Some(msgs) = debug_messages.as_mut() {
1186 msgs.push(LayoutDebugMessage::info(format!(
1187 "[layout_and_generate_display_list] Layout SUCCESS, layout_results count: {}",
1188 self.layout_results.len()
1189 )));
1190 }
1191
1192 #[cfg(feature = "a11y")]
1194 if result.is_ok() {
1195 self.update_a11y_tree();
1196 }
1197
1198 if result.is_ok() {
1200 self.scroll_focused_cursor_into_view();
1201 }
1202
1203 result
1204 }
1205
1206 #[cfg(feature = "std")]
1231 pub fn measure_dom(&self, dom: Dom, available: LogicalSize) -> LogicalSize {
1232 let styled_dom = StyledDom::create_from_dom(dom);
1233 self.measure_styled_dom(&styled_dom, available)
1234 }
1235
1236 #[cfg(feature = "std")]
1238 pub fn measure_styled_dom(
1239 &self,
1240 styled_dom: &StyledDom,
1241 available: LogicalSize,
1242 ) -> LogicalSize {
1243 let mut scratch_cache = Solver3LayoutCache {
1244 tree: None,
1245 calculated_positions: Vec::new(),
1246 viewport: None,
1247 scroll_ids: HashMap::new(),
1248 scroll_id_to_node_id: HashMap::new(),
1249 counters: HashMap::new(),
1250 float_cache: HashMap::new(),
1251 cache_map: solver3::cache::LayoutCacheMap::default(),
1252 previous_positions: Vec::new(),
1253 cached_display_list: None,
1254 prev_dom_ptr: 0,
1255 prev_viewport: LogicalRect::zero(),
1256 };
1257 let mut scratch_text = TextLayoutCache::new();
1258 let viewport = LogicalRect::new(LogicalPosition::zero(), available);
1259 let external = ExternalSystemCallbacks::rust_internal();
1260
1261 let layout_result = solver3::layout_document(
1262 &mut scratch_cache,
1263 &mut scratch_text,
1264 styled_dom,
1265 viewport,
1266 &self.font_manager,
1267 &BTreeMap::new(),
1268 &BTreeMap::new(),
1269 &mut None,
1270 None, &self.renderer_resources,
1272 self.id_namespace,
1273 styled_dom.dom_id,
1274 false,
1275 Vec::new(),
1276 None,
1277 &self.image_cache,
1278 self.system_style.clone(),
1279 external.get_system_time_fn,
1280 );
1281 if layout_result.is_err() {
1282 return LogicalSize::zero();
1283 }
1284
1285 let Some(tree) = scratch_cache.tree.as_ref() else {
1288 return LogicalSize::zero();
1289 };
1290 let mut max_x = 0.0f32;
1291 let mut max_y = 0.0f32;
1292 for (idx, node) in tree.nodes.iter().enumerate() {
1293 let Some(size) = node.used_size else { continue };
1294 let pos = solver3::pos_get(&scratch_cache.calculated_positions, idx)
1295 .unwrap_or(LogicalPosition::zero());
1296 max_x = max_x.max(pos.x + size.width);
1297 max_y = max_y.max(pos.y + size.height);
1298 }
1299 LogicalSize::new(max_x, max_y)
1300 }
1301
1302 pub fn layout_dom_recursive(
1307 &mut self,
1308 styled_dom: StyledDom,
1309 window_state: &FullWindowState,
1310 renderer_resources: &RendererResources,
1311 system_callbacks: &ExternalSystemCallbacks,
1312 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1313 ) -> Result<(), solver3::LayoutError> {
1314 let is_child_dom = styled_dom.dom_id.inner != 0;
1325 if is_child_dom {
1326 let saved_root_cache = core::mem::take(&mut self.layout_cache);
1327 let result = self.layout_dom_recursive_impl(
1328 styled_dom,
1329 window_state,
1330 renderer_resources,
1331 system_callbacks,
1332 debug_messages,
1333 );
1334 self.layout_cache = saved_root_cache;
1335 return result;
1336 }
1337 self.layout_dom_recursive_impl(
1338 styled_dom,
1339 window_state,
1340 renderer_resources,
1341 system_callbacks,
1342 debug_messages,
1343 )
1344 }
1345
1346 #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)] #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] fn layout_dom_recursive_impl(
1349 &mut self,
1350 styled_dom: StyledDom,
1351 window_state: &FullWindowState,
1352 renderer_resources: &RendererResources,
1353 system_callbacks: &ExternalSystemCallbacks,
1354 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1355 ) -> Result<(), solver3::LayoutError> {
1356 static MEM_BREAKDOWN_ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1360 static CPU_ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1366 static CASCADE_ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1370
1371 let dom_id = if styled_dom.dom_id.inner == 0 {
1372 DomId::ROOT_ID
1373 } else {
1374 styled_dom.dom_id
1375 };
1376
1377 if dom_id != DomId::ROOT_ID {
1381 self.layout_cache.reset_incremental();
1382 }
1383
1384 let viewport = LogicalRect {
1385 origin: LogicalPosition::zero(),
1386 size: window_state.size.dimensions,
1387 };
1388
1389 let platform = self.system_style.as_ref().map_or_else(azul_css::system::Platform::current, |s| s.platform.clone());
1391
1392 {
1395 use crate::{
1396 solver3::getters::collect_and_resolve_font_chains_with_registration,
1397 text3::default::PathLoader,
1398 };
1399
1400 let compact_cache_ref = styled_dom.css_property_cache.ptr.compact_cache.as_ref();
1416 let font_dirty_count = compact_cache_ref
1417 .map_or(1, |cc| cc.font_dirty_nodes.len()); let font_stacks_sig = compact_cache_ref.map(|cc| {
1420 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
1426 for &fh in &cc.prev_font_hashes {
1427 h = h.rotate_left(13) ^ fh;
1428 h = h.wrapping_mul(0x0100_0000_01b3);
1429 }
1430 h
1431 });
1432
1433 let font_requirements_unchanged = font_dirty_count == 0
1450 && font_stacks_sig.is_some()
1451 && font_stacks_sig == self.font_manager.last_resolved_font_stacks_sig
1452 && !self.font_manager.font_chain_cache.is_empty();
1453
1454 if font_requirements_unchanged {
1455 if let Some(msgs) = debug_messages.as_mut() {
1456 msgs.push(LayoutDebugMessage::info(
1457 "[FontLoading] Font requirements unchanged, skipping resolution (cached)".to_string(),
1458 ));
1459 }
1460 } else {
1461 if let Some(msgs) = debug_messages.as_mut() {
1462 msgs.push(LayoutDebugMessage::info(
1463 "[FontLoading] Starting font resolution for DOM".to_string(),
1464 ));
1465 }
1466
1467 if let Some(cc) = styled_dom.css_property_cache.ptr.compact_cache.as_ref() {
1470 for (k, v) in &cc.font_hash_to_families {
1471 self.font_manager.font_hash_to_families.insert(*k, v.clone());
1472 }
1473 }
1474
1475 crate::probe::sample_peak_rss("rss:before_font_chain");
1483 let mut chains = {
1484 let _p = crate::probe::Probe::span("font_chain_resolve");
1485 collect_and_resolve_font_chains_with_registration(
1486 &styled_dom, &self.font_manager.fc_cache, &self.font_manager, &platform,
1487 )
1488 };
1489 unsafe { crate::az_mark(0x60770_u32, chains.chains.len() as u32); }
1491 for chain in chains.chains.values_mut() {
1499 let total = chain.css_fallbacks.iter().map(|g| g.fonts.len()).sum::<usize>()
1500 + chain.unicode_fallbacks.len();
1501 if total == 0 {
1502 if let Some((pattern, id)) = self.font_manager.fc_cache.list().first() {
1503 chain.unicode_fallbacks.push(rust_fontconfig::FontMatch {
1504 id: *id,
1505 unicode_ranges: pattern.unicode_ranges.clone(),
1506 fallbacks: Vec::new(),
1507 });
1508 }
1509 }
1510 }
1511 unsafe { crate::az_mark(0x60774_u32, chains.chains.len() as u32); }
1513 crate::probe::sample_peak_rss("rss:after_font_chain");
1523
1524 if let Some(msgs) = debug_messages.as_mut() {
1533 msgs.push(LayoutDebugMessage::info(format!(
1534 "[FontLoading] Resolved {} font chains",
1535 chains.len()
1536 )));
1537 }
1538
1539 let loader = PathLoader::new();
1540 crate::probe::sample_peak_rss("rss:before_font_load");
1541 let failed = {
1542 let _p = crate::probe::Probe::span("font_load_missing");
1543 self.font_manager.load_missing_for_chains(
1544 &chains,
1545 |bytes, index| loader.load_font_shared(bytes, index),
1546 )
1547 };
1548 crate::probe::sample_peak_rss("rss:after_font_load");
1549 if let Some(msgs) = debug_messages.as_mut() {
1550 for (font_id, error) in &failed {
1551 msgs.push(LayoutDebugMessage::warning(format!(
1552 "[FontLoading] Failed to load font {font_id:?}: {error}"
1553 )));
1554 }
1555 }
1556
1557 let single_dom = self
1572 .layout_results
1573 .keys()
1574 .all(|d| *d == dom_id);
1575 if single_dom {
1576 let keep_ids =
1577 solver3::getters::collect_font_ids_from_chains(&chains);
1578 let keep_hashes: std::collections::HashSet<u64> = styled_dom
1579 .css_property_cache
1580 .ptr
1581 .compact_cache
1582 .as_ref()
1583 .map(|cc| cc.font_hash_to_families.keys().copied().collect())
1584 .unwrap_or_default();
1585 let evicted = self
1586 .font_manager
1587 .garbage_collect_fonts(&keep_ids, &keep_hashes);
1588 if evicted > 0 {
1589 if let Some(msgs) = debug_messages.as_mut() {
1590 msgs.push(LayoutDebugMessage::info(format!(
1591 "[FontLoading] GC evicted {evicted} unreferenced font(s)"
1592 )));
1593 }
1594 }
1595 }
1596
1597 let fc_chains = chains.into_fontconfig_chains();
1601 unsafe { crate::az_mark(0x60778_u32, fc_chains.len() as u32); }
1603 self.font_manager.set_font_chain_cache_with_sig(
1604 fc_chains,
1605 font_stacks_sig,
1606 );
1607 unsafe { crate::az_mark(0x6077C_u32, (self.font_manager.font_chain_cache.len() as u32)); }
1609 }
1610 }
1611 let scroll_offsets = self.scroll_manager.get_scroll_states_for_dom(dom_id);
1612
1613 if !self.skip_gpu_sync {
1630 let mut transform_opacity_events = self
1631 .gpu_state_manager
1632 .get_or_create_cache(dom_id)
1633 .synchronize(&styled_dom);
1634 drop(self.gpu_state_manager.take_pending_changes());
1643 self.gpu_state_manager
1644 .pending_changes
1645 .merge(&mut transform_opacity_events);
1646 }
1647 let gpu_cache = if self.skip_gpu_sync {
1652 GpuValueCache::default()
1653 } else {
1654 self.gpu_state_manager.get_or_create_cache(dom_id).clone()
1655 };
1656
1657 let cursor_is_visible = self.text_edit_manager.should_draw_cursor();
1658 let cursor_locations = self.text_edit_manager.build_cursor_locations();
1659
1660 let mut display_list = {
1661 let _p = crate::probe::Probe::span("solver3_layout_document");
1662 solver3::layout_document(
1663 &mut self.layout_cache,
1664 &mut self.text_cache,
1665 &styled_dom,
1666 viewport,
1667 &self.font_manager,
1668 &scroll_offsets,
1669 &BTreeMap::new(),
1670 debug_messages,
1671 Some(&gpu_cache),
1672 &self.renderer_resources,
1673 self.id_namespace,
1674 dom_id,
1675 cursor_is_visible,
1676 cursor_locations,
1677 self.text_edit_manager.preedit_text.clone(),
1678 &self.image_cache,
1679 self.system_style.clone(),
1680 system_callbacks.get_system_time_fn,
1681 )?
1682 };
1683
1684 crate::probe::hint_purge_allocator();
1687
1688 if self.skip_gpu_sync {
1699 if let Some(tree) = self.layout_cache.tree.clone() {
1700 self.layout_results.insert(
1701 dom_id,
1702 DomLayoutResult {
1703 styled_dom,
1704 layout_tree: tree,
1705 calculated_positions: self.layout_cache.calculated_positions.clone(),
1706 viewport,
1707 display_list: DisplayList::default(),
1708 scroll_ids: self.layout_cache.scroll_ids.clone(),
1709 scroll_id_to_node_id: self.layout_cache.scroll_id_to_node_id.clone(),
1710 },
1711 );
1712 }
1713 return Ok(());
1714 }
1715
1716 if *MEM_BREAKDOWN_ENABLED.get_or_init(azul_core::profile::memory_enabled) {
1717 let sr = styled_dom.memory_report();
1718 eprintln!("[MEM] StyledDom ({} nodes) total={} KiB", sr.node_count, sr.total_bytes() / 1024);
1719 eprintln!("[MEM] node_hierarchy {:>7} KiB", sr.node_hierarchy_bytes / 1024);
1720 eprintln!("[MEM] node_data {:>7} KiB", sr.node_data_bytes / 1024);
1721 eprintln!("[MEM] styled_nodes {:>7} KiB", sr.styled_nodes_bytes / 1024);
1722 eprintln!("[MEM] cascade_info {:>7} KiB", sr.cascade_info_bytes / 1024);
1723 eprintln!("[MEM] tag_ids {:>7} KiB", sr.tag_ids_bytes / 1024);
1724 eprintln!("[MEM] non_leaf_nodes {:>7} KiB", sr.non_leaf_nodes_bytes / 1024);
1725 let bd = &sr.css_property_cache;
1726 eprintln!("[MEM] CssPropertyCache {:>7} KiB", bd.total_bytes() / 1024);
1727 eprintln!("[MEM] cascaded_props {:>6} KiB", bd.cascaded_props_bytes / 1024);
1728 eprintln!("[MEM] css_props {:>6} KiB", bd.css_props_bytes / 1024);
1729 eprintln!("[MEM] computed_values {:>7} KiB", bd.computed_values_bytes / 1024);
1730 eprintln!("[MEM] user_overridden {:>7} KiB", bd.user_overridden_bytes / 1024);
1731 eprintln!("[MEM] global_css_props {:>7} KiB", bd.global_css_props_bytes / 1024);
1732 eprintln!("[MEM] compact_cache {:>7} KiB", bd.compact_cache_bytes / 1024);
1733 eprintln!("[MEM] resolved_font_sz {:>7} KiB", bd.resolved_font_sizes_bytes / 1024);
1734
1735 let sc = self.layout_cache.memory_report();
1737 eprintln!("[MEM] Solver3 LayoutCache total={} KiB", sc.total_bytes() / 1024);
1738 if let Some(tr) = &sc.tree_report {
1739 eprintln!("[MEM] LayoutTree {:>7} KiB ({} nodes)", sc.tree_bytes / 1024, tr.node_count);
1740 eprintln!("[MEM] hot {:>6} KiB", tr.hot_bytes / 1024);
1741 eprintln!("[MEM] warm {:>6} KiB", tr.warm_bytes / 1024);
1742 eprintln!("[MEM] warm.inline {:>6} KiB (shaped text in CachedInlineLayout)", tr.warm_inline_layout_bytes / 1024);
1743 eprintln!("[MEM] warm.taffy {:>6} KiB", tr.warm_taffy_cache_bytes / 1024);
1744 eprintln!("[MEM] cold {:>6} KiB", tr.cold_bytes / 1024);
1745 eprintln!("[MEM] children_arena {:>6} KiB", tr.children_arena_bytes / 1024);
1746 eprintln!("[MEM] dom_to_layout {:>6} KiB", tr.dom_to_layout_bytes / 1024);
1747 }
1748 eprintln!("[MEM] cache_map {:>7} KiB (Taffy-style 9+1 slots per node)", sc.cache_map_bytes / 1024);
1749 eprintln!("[MEM] calculated_pos {:>7} KiB", sc.calculated_positions_bytes / 1024);
1750 eprintln!("[MEM] previous_pos {:>7} KiB", sc.previous_positions_bytes / 1024);
1751 eprintln!("[MEM] float_cache {:>7} KiB", sc.float_cache_bytes / 1024);
1752 eprintln!("[MEM] counters {:>7} KiB", sc.counters_bytes / 1024);
1753 eprintln!("[MEM] scroll_ids {:>7} KiB", sc.scroll_ids_bytes / 1024);
1754 eprintln!("[MEM] cached_display {:>7} KiB", sc.cached_display_list_bytes / 1024);
1755
1756 let tc = self.text_cache.memory_report();
1758 eprintln!("[MEM] TextShapingCache total={} KiB", tc.total_bytes() / 1024);
1759 eprintln!("[MEM] logical_items {:>7} KiB ({} entries)", tc.logical_items_bytes / 1024, tc.logical_items_entries);
1760 eprintln!("[MEM] visual_items {:>7} KiB ({} entries)", tc.visual_items_bytes / 1024, tc.visual_items_entries);
1761 eprintln!("[MEM] shaped_items {:>7} KiB ({} entries)", tc.shaped_items_bytes / 1024, tc.shaped_items_entries);
1762 eprintln!("[MEM] glyph_bytes {:>7} KiB", tc.shaped_glyph_bytes / 1024);
1763 eprintln!("[MEM] cluster_text {:>7} KiB", tc.shaped_cluster_text_bytes / 1024);
1764 eprintln!("[MEM] per_item_shaped {:>7} KiB ({} entries)", tc.per_item_shaped_bytes / 1024, tc.per_item_shaped_entries);
1765
1766 let grand_total = sr.total_bytes() + sc.total_bytes() + tc.total_bytes();
1767 eprintln!("[MEM] --- GRAND TOTAL (StyledDom + Solver3 + TextCache) = {} KiB = {:.2} MiB ---",
1768 grand_total / 1024, grand_total as f64 / 1_048_576.0);
1769
1770 #[cfg(feature = "probe")]
1771 {
1772 let (rss, _virt) = crate::probe::current_rss_bytes();
1773 let peak = crate::probe::peak_rss_bytes_pub();
1774 eprintln!("[MEM] after layout: current rss={:.1} MiB peak rss={:.1} MiB (unreturned={:.1} MiB)",
1775 rss as f64 / 1048576.0, peak as f64 / 1048576.0,
1776 (peak.saturating_sub(rss)) as f64 / 1048576.0);
1777 eprintln!("[MEM] accounted / rss = {:.1}% — the gap is allocator overhead + unreturned transient pages + fonts/images + misc",
1778 grand_total as f64 * 100.0 / (rss as f64).max(1.0));
1779 }
1780 }
1781
1782 if *CPU_ENABLED.get_or_init(azul_core::profile::cpu_enabled) {
1783 let events = crate::probe::Probe::drain();
1784 crate::probe::print_drained_events("layout pass", &events);
1785 }
1786
1787 if *CASCADE_ENABLED.get_or_init(azul_core::profile::cascade_enabled) {
1788 let counts = azul_core::prop_cache::drain_css_prop_counts();
1789 let total: usize = counts.iter().map(|(_, n)| *n).sum();
1790 if total > 0 {
1791 eprintln!("[CASCADE] cascade-walks this pass: {total} total");
1792 for (label, n) in counts.iter().take(20) {
1793 eprintln!("[CASCADE] {n:>8} {label}");
1794 }
1795 }
1796 }
1797
1798 let tree = self
1799 .layout_cache
1800 .tree
1801 .clone()
1802 .ok_or(solver3::LayoutError::InvalidTree)?;
1803
1804 let scroll_ids = self.layout_cache.scroll_ids.clone();
1806 let scroll_id_to_node_id = self.layout_cache.scroll_id_to_node_id.clone();
1807
1808 {
1814 use crate::solver3::display_list::{DisplayListItem, ScrollbarDrawInfo};
1815 let gpu_cache = self.gpu_state_manager.get_or_create_cache(dom_id);
1816 for item in &display_list.items {
1817 if let DisplayListItem::ScrollBarStyled { info } = item {
1818 if let Some(hit_id) = &info.hit_id {
1819 if let Some(transform_key) = info.thumb_transform_key {
1821 match hit_id {
1822 ScrollbarHitId::VerticalThumb(_, nid) => {
1823 if !gpu_cache.transform_keys.contains_key(nid) {
1824 gpu_cache.transform_keys.insert(*nid, transform_key);
1825 gpu_cache.current_transform_values.insert(*nid, info.thumb_initial_transform);
1826 }
1827 }
1828 ScrollbarHitId::HorizontalThumb(_, nid) => {
1829 if !gpu_cache.h_transform_keys.contains_key(nid) {
1830 gpu_cache.h_transform_keys.insert(*nid, transform_key);
1831 gpu_cache.h_current_transform_values.insert(*nid, info.thumb_initial_transform);
1832 }
1833 }
1834 _ => {}
1835 }
1836 }
1837
1838 let initial_opacity = if info.visibility == azul_css::props::style::scrollbar::ScrollbarVisibilityMode::Always {
1849 1.0
1850 } else {
1851 0.0
1852 };
1853 if let Some(opacity_key) = info.opacity_key {
1854 match hit_id {
1855 ScrollbarHitId::VerticalThumb(_, nid) => {
1856 let key = (dom_id, *nid);
1857 if let std::collections::hash_map::Entry::Vacant(e) = gpu_cache.scrollbar_v_opacity_keys.entry(key) {
1858 e.insert(opacity_key);
1859 gpu_cache.scrollbar_v_opacity_values.insert(key, initial_opacity);
1860 }
1861 }
1862 ScrollbarHitId::HorizontalThumb(_, nid) => {
1863 let key = (dom_id, *nid);
1864 if let std::collections::hash_map::Entry::Vacant(e) = gpu_cache.scrollbar_h_opacity_keys.entry(key) {
1865 e.insert(opacity_key);
1866 gpu_cache.scrollbar_h_opacity_values.insert(key, initial_opacity);
1867 }
1868 }
1869 _ => {}
1870 }
1871 }
1872 }
1873 }
1874 }
1875 }
1876
1877 self.gpu_state_manager
1879 .update_scrollbar_transforms(dom_id, &self.scroll_manager, &tree);
1880
1881 let vviews = Self::scan_for_virtual_views(&styled_dom, &tree, &self.layout_cache.calculated_positions);
1884
1885 if std::env::var("AZ_MAP_DEBUG").is_ok() {
1886 eprintln!("[vview] scan found {} VirtualView node(s): {:?}", vviews.len(),
1887 vviews.iter().map(|(n, b)| (n.index(), b.origin.x, b.origin.y, b.size.width, b.size.height)).collect::<Vec<_>>());
1888 }
1889
1890 for (node_id, bounds) in vviews {
1891 if let Some(child_dom_id) = self.invoke_virtual_view_callback_with_dom(
1892 dom_id,
1893 node_id,
1894 bounds,
1895 Some(&styled_dom),
1896 window_state,
1897 renderer_resources,
1898 system_callbacks,
1899 debug_messages,
1900 ) {
1901 let mut replaced = false;
1905 for item in &mut display_list.items {
1906 if let solver3::display_list::DisplayListItem::VirtualViewPlaceholder {
1907 node_id: ref placeholder_nid,
1908 bounds: ref placeholder_bounds,
1909 clip_rect: ref placeholder_clip,
1910 ..
1911 } = item
1912 {
1913 if *placeholder_nid == node_id {
1914 if std::env::var("AZ_MAP_DEBUG").is_ok() {
1915 eprintln!(
1916 "[vview] placeholder swap: node={} placeholder_bounds={:?} scan_bounds={:?}",
1917 node_id.index(), placeholder_bounds.inner(), bounds
1918 );
1919 }
1920 *item = solver3::display_list::DisplayListItem::VirtualView {
1921 child_dom_id,
1922 bounds: *placeholder_bounds,
1923 clip_rect: *placeholder_clip,
1924 };
1925 replaced = true;
1926 break;
1927 }
1928 }
1929 }
1930
1931 if !replaced {
1932 display_list
1934 .items
1935 .push(solver3::display_list::DisplayListItem::VirtualView {
1936 child_dom_id,
1937 bounds: bounds.into(),
1938 clip_rect: bounds.into(),
1939 });
1940 }
1941 }
1942 }
1943
1944 self.layout_results.insert(
1947 dom_id,
1948 DomLayoutResult {
1949 styled_dom,
1950 layout_tree: tree,
1951 calculated_positions: self.layout_cache.calculated_positions.clone(),
1952 viewport,
1953 display_list,
1954 scroll_ids,
1955 scroll_id_to_node_id,
1956 },
1957 );
1958
1959 self.scroll_manager.clear_scroll_dirty();
1962
1963 self.text_edit_manager.display_list_dirty = false;
1977
1978 Ok(())
1979 }
1980
1981 fn scan_for_virtual_views(
1982 styled_dom: &StyledDom,
1983 layout_tree: &LayoutTree,
1984 calculated_positions: &solver3::PositionVec,
1985 ) -> Vec<(NodeId, LogicalRect)> {
1986 let node_data_container = styled_dom.node_data.as_container();
1987 layout_tree
1988 .nodes
1989 .iter()
1990 .enumerate()
1991 .filter_map(|(idx, node)| {
1992 let node_dom_id = node.dom_node_id?;
1993 let node_data = node_data_container.get(node_dom_id)?;
1994 if matches!(node_data.get_node_type(), NodeType::VirtualView) {
1995 let pos = calculated_positions.get(idx).copied().unwrap_or_default();
1996 let size = node.used_size.unwrap_or_default();
1997 Some((node_dom_id, LogicalRect::new(pos, size)))
1998 } else {
1999 None
2000 }
2001 })
2002 .collect()
2003 }
2004
2005 pub fn invoke_cpu_image_callbacks(&mut self, gl_context: &OptionGlContextPtr) {
2023 use azul_core::resources::DecodedImage;
2024
2025 let hidpi_factor = self.current_window_state.size.get_hidpi_factor();
2027 let mut to_invoke: Vec<(DomId, NodeId, ImageRefHash, HidpiAdjustedBounds, ImageRef)> =
2028 Vec::new();
2029 for (dom_id, lr) in &self.layout_results {
2030 let node_data_container = lr.styled_dom.node_data.as_container();
2031 for (idx, node) in lr.layout_tree.nodes.iter().enumerate() {
2032 let Some(node_dom_id) = node.dom_node_id else {
2033 continue;
2034 };
2035 let Some(node_data) = node_data_container.get(node_dom_id) else {
2036 continue;
2037 };
2038 if let NodeType::Image(image_ref) = node_data.get_node_type() {
2039 if !matches!(image_ref.get_data(), DecodedImage::Callback(_)) {
2040 continue;
2041 }
2042 let _ = idx;
2043 let size = node.used_size.unwrap_or_default();
2044 let bounds = HidpiAdjustedBounds {
2045 logical_size: size,
2046 hidpi_factor,
2047 };
2048 to_invoke.push((
2049 *dom_id,
2050 node_dom_id,
2051 image_ref.get_hash(),
2052 bounds,
2053 (**image_ref).clone(),
2056 ));
2057 }
2058 }
2059 }
2060
2061 if to_invoke.is_empty() {
2062 self.cpu_image_callback_results.clear();
2063 return;
2064 }
2065
2066 let mut results: BTreeMap<ImageRefHash, ImageRef> = BTreeMap::new();
2072 for (dom_id, node_id, hash, bounds, image_ref) in to_invoke {
2073 let domnode_id = DomNodeId {
2074 dom: dom_id,
2075 node: NodeHierarchyItemId::from_crate_internal(Some(node_id)),
2076 };
2077 let info = crate::callbacks::RenderImageCallbackInfo::new(
2078 domnode_id,
2079 bounds,
2080 gl_context,
2081 &self.image_cache,
2082 &self.font_manager.fc_cache,
2083 );
2084 let produced = match image_ref.get_data() {
2085 DecodedImage::Callback(core_callback) if core_callback.callback.cb != 0 => {
2086 let cb = crate::callbacks::RenderImageCallback::from_core(&core_callback.callback);
2087 let refany = core_callback.refany.clone();
2088 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| (cb.cb)(refany, info)))
2089 .ok()
2090 }
2091 _ => None,
2092 };
2093 if let Some(img) = produced {
2094 results.insert(hash, img);
2095 }
2096 }
2097 self.cpu_image_callback_results = results;
2098 }
2099
2100 pub fn resize_window(
2110 &mut self,
2111 styled_dom: StyledDom,
2112 new_size: LogicalSize,
2113 renderer_resources: &RendererResources,
2114 system_callbacks: &ExternalSystemCallbacks,
2115 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
2116 ) -> Result<DisplayList, solver3::LayoutError> {
2117 let mut window_state = FullWindowState::default();
2119 window_state.size.dimensions = new_size;
2120
2121 let dom_id = styled_dom.dom_id;
2122
2123 self.layout_and_generate_display_list(
2124 styled_dom,
2125 &window_state,
2126 renderer_resources,
2127 system_callbacks,
2128 debug_messages,
2129 )?;
2130
2131 self.layout_results
2134 .get_mut(&dom_id)
2135 .map(|result| std::mem::take(&mut result.display_list))
2136 .ok_or(solver3::LayoutError::InvalidTree)
2137 }
2138
2139 pub fn clear_caches(&mut self) {
2141 self.layout_cache = Solver3LayoutCache {
2142 tree: None,
2143 calculated_positions: Vec::new(),
2144 viewport: None,
2145 scroll_ids: HashMap::new(),
2146 scroll_id_to_node_id: HashMap::new(),
2147 counters: HashMap::new(),
2148 float_cache: HashMap::new(),
2149 cache_map: solver3::cache::LayoutCacheMap::default(),
2150 previous_positions: Vec::new(),
2151 cached_display_list: None,
2152 prev_dom_ptr: 0,
2153 prev_viewport: LogicalRect::zero(),
2154 };
2155 self.text_cache = TextLayoutCache::new();
2156 self.layout_results.clear();
2157 self.scroll_manager = ScrollManager::new();
2158 }
2159
2160 pub fn set_scroll_position(&mut self, dom_id: DomId, node_id: NodeId, scroll: ScrollPosition) {
2162 #[cfg(feature = "std")]
2164 let now = Instant::now();
2165 #[cfg(not(feature = "std"))]
2166 let now = Instant::Tick(azul_core::task::SystemTick { tick_counter: 0 });
2167
2168 self.scroll_manager.update_node_bounds(
2169 dom_id,
2170 node_id,
2171 scroll.parent_rect,
2172 scroll.children_rect,
2173 now.clone(),
2174 );
2175 self.scroll_manager
2176 .set_scroll_position(dom_id, node_id, scroll.children_rect.origin, now);
2177 }
2178
2179 pub fn get_scroll_position(&self, dom_id: DomId, node_id: NodeId) -> Option<ScrollPosition> {
2181 let states = self.scroll_manager.get_scroll_states_for_dom(dom_id);
2182 states.get(&node_id).copied()
2183 }
2184
2185 pub fn set_selection(&mut self, _dom_id: DomId, _selection: SelectionState) {
2187 }
2189
2190 pub const fn get_selection(&self, _dom_id: DomId) -> Option<&SelectionState> {
2192 None
2193 }
2194
2195 pub fn invoke_virtual_view_callback(
2205 &mut self,
2206 parent_dom_id: DomId,
2207 node_id: NodeId,
2208 bounds: LogicalRect,
2209 window_state: &FullWindowState,
2210 renderer_resources: &RendererResources,
2211 system_callbacks: &ExternalSystemCallbacks,
2212 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
2213 ) -> Option<DomId> {
2214 self.invoke_virtual_view_callback_with_dom(
2215 parent_dom_id, node_id, bounds, None,
2216 window_state, renderer_resources, system_callbacks, debug_messages,
2217 )
2218 }
2219
2220 fn invoke_virtual_view_callback_with_dom(
2224 &mut self,
2225 parent_dom_id: DomId,
2226 node_id: NodeId,
2227 bounds: LogicalRect,
2228 styled_dom_override: Option<&StyledDom>,
2229 window_state: &FullWindowState,
2230 renderer_resources: &RendererResources,
2231 system_callbacks: &ExternalSystemCallbacks,
2232 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
2233 ) -> Option<DomId> {
2234 if let Some(msgs) = debug_messages {
2235 msgs.push(LayoutDebugMessage::info(format!(
2236 "invoke_virtual_view_callback called for node {node_id:?}"
2237 )));
2238 }
2239
2240 let virtual_view_node = if let Some(styled_dom) = styled_dom_override {
2242 let node_data_container = styled_dom.node_data.as_container();
2243 let node_data = node_data_container.get(node_id)?;
2244 node_data.get_virtual_view_node_ref()?.clone()
2245 } else {
2246 let layout_result = self.layout_results.get(&parent_dom_id)?;
2247 if let Some(msgs) = debug_messages {
2248 msgs.push(LayoutDebugMessage::info(format!(
2249 "Got layout result for parent DOM {parent_dom_id:?}"
2250 )));
2251 }
2252 let node_data_container = layout_result.styled_dom.node_data.as_container();
2253 let node_data = node_data_container.get(node_id)?;
2254 if let Some(vv) = node_data.get_virtual_view_node_ref() { vv.clone() } else {
2255 if let Some(msgs) = debug_messages {
2256 msgs.push(LayoutDebugMessage::info(format!(
2257 "Node is NOT VirtualView, type = {:?}",
2258 node_data.get_node_type()
2259 )));
2260 }
2261 return None;
2262 }
2263 };
2264
2265 if let Some(msgs) = debug_messages {
2266 msgs.push(LayoutDebugMessage::info("Node is VirtualView type".to_string()));
2267 }
2268
2269 self.invoke_virtual_view_callback_impl(
2271 parent_dom_id,
2272 node_id,
2273 &virtual_view_node,
2274 bounds,
2275 window_state,
2276 renderer_resources,
2277 system_callbacks,
2278 debug_messages,
2279 )
2280 }
2281
2282 #[allow(clippy::too_many_lines)] fn invoke_virtual_view_callback_impl(
2294 &mut self,
2295 parent_dom_id: DomId,
2296 node_id: NodeId,
2297 virtual_view_node: &azul_core::dom::VirtualViewNode,
2298 bounds: LogicalRect,
2299 window_state: &FullWindowState,
2300 renderer_resources: &RendererResources,
2301 system_callbacks: &ExternalSystemCallbacks,
2302 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
2303 ) -> Option<DomId> {
2304 let now = (system_callbacks.get_system_time_fn.cb)();
2306
2307 self.scroll_manager.update_node_bounds(
2310 parent_dom_id,
2311 node_id,
2312 bounds,
2313 LogicalRect::new(LogicalPosition::zero(), bounds.size), now,
2315 );
2316
2317 let Some(reason) = self.virtual_view_manager.check_reinvoke(
2320 parent_dom_id,
2321 node_id,
2322 &self.scroll_manager,
2323 bounds,
2324 ) else {
2325 return self
2327 .virtual_view_manager
2328 .get_nested_dom_id(parent_dom_id, node_id);
2329 };
2330
2331 if let Some(msgs) = debug_messages {
2332 msgs.push(LayoutDebugMessage::info(format!(
2333 "VirtualView ({parent_dom_id:?}, {node_id:?}) - Reason: {reason:?}"
2334 )));
2335 }
2336
2337 let scroll_offset = self
2338 .scroll_manager
2339 .get_current_offset(parent_dom_id, node_id)
2340 .unwrap_or_default();
2341
2342 let hidpi_factor = window_state.size.get_hidpi_factor();
2343
2344 let mut callback_info = azul_core::callbacks::VirtualViewCallbackInfo::new(
2346 reason,
2347 &self.font_manager.fc_cache,
2348 &self.image_cache,
2349 window_state.theme,
2350 HidpiAdjustedBounds {
2351 logical_size: bounds.size,
2352 hidpi_factor,
2353 },
2354 bounds.size,
2355 scroll_offset,
2356 bounds.size,
2357 LogicalPosition::zero(),
2358 );
2359 #[cfg(feature = "std")]
2364 callback_info.set_measure_dom_fn(
2365 virtual_view_measure_dom_trampoline,
2366 core::ptr::from_mut::<Self>(self).cast(),
2367 );
2368
2369 let callback_data = virtual_view_node.refany.clone();
2371
2372 let callback_return = (virtual_view_node.callback.cb)(callback_data, callback_info);
2374
2375 self.virtual_view_manager
2377 .mark_invoked(parent_dom_id, node_id, reason);
2378
2379 let mut child_styled_dom = match callback_return.dom {
2381 azul_core::dom::OptionDom::Some(dom) => {
2382 StyledDom::create_from_dom(dom)
2384 },
2385 azul_core::dom::OptionDom::None => {
2386 if reason == VirtualViewCallbackReason::InitialRender {
2388 let mut empty_dom = Dom::create_div();
2390 let empty_css = Css::empty();
2391 StyledDom::create(&mut empty_dom, empty_css)
2392 } else {
2393 self.virtual_view_manager.update_virtual_view_info(
2396 parent_dom_id,
2397 node_id,
2398 callback_return.scroll_size,
2399 callback_return.virtual_scroll_size,
2400 );
2401 self.scroll_manager.update_virtual_scroll_bounds(
2403 parent_dom_id,
2404 node_id,
2405 callback_return.virtual_scroll_size,
2406 Some(callback_return.scroll_offset),
2407 );
2408 return self
2409 .virtual_view_manager
2410 .get_nested_dom_id(parent_dom_id, node_id);
2411 }
2412 }
2413 };
2414
2415 let child_dom_id = self
2417 .virtual_view_manager
2418 .get_or_create_nested_dom_id(parent_dom_id, node_id);
2419 child_styled_dom.dom_id = child_dom_id;
2420
2421 self.virtual_view_manager.update_virtual_view_info(
2423 parent_dom_id,
2424 node_id,
2425 callback_return.scroll_size,
2426 callback_return.virtual_scroll_size,
2427 );
2428 self.scroll_manager.update_virtual_scroll_bounds(
2430 parent_dom_id,
2431 node_id,
2432 callback_return.virtual_scroll_size,
2433 Some(callback_return.scroll_offset),
2434 );
2435
2436 self.layout_dom_recursive(
2441 child_styled_dom,
2442 window_state,
2443 renderer_resources,
2444 system_callbacks,
2445 debug_messages,
2446 )
2447 .ok()?;
2448
2449 Some(child_dom_id)
2450 }
2451
2452 pub fn get_node_size(&self, node_id: DomNodeId) -> Option<LogicalSize> {
2456 let layout_result = self.layout_results.get(&node_id.dom)?;
2457 let nid = node_id.node.into_crate_internal()?;
2458 let layout_indices = layout_result.layout_tree.dom_to_layout.get(&nid)?;
2460 let layout_index = *layout_indices.first()?;
2461 let layout_node = layout_result.layout_tree.get(layout_index)?;
2462 layout_node.used_size
2463 }
2464
2465 pub fn get_node_position(&self, node_id: DomNodeId) -> Option<LogicalPosition> {
2467 let layout_result = self.layout_results.get(&node_id.dom)?;
2468 let nid = node_id.node.into_crate_internal()?;
2469 let layout_indices = layout_result.layout_tree.dom_to_layout.get(&nid)?;
2471 let layout_index = *layout_indices.first()?;
2472 let position = layout_result.calculated_positions.get(layout_index)?;
2473 Some(*position)
2474 }
2475
2476 pub fn get_node_hit_test_bounds(&self, node_id: DomNodeId) -> Option<LogicalRect> {
2482 use crate::solver3::display_list::DisplayListItem;
2483
2484 let layout_result = self.layout_results.get(&node_id.dom)?;
2485 let nid = node_id.node.into_crate_internal()?;
2486
2487 let nid_encoded = NodeHierarchyItemId::from_crate_internal(Some(nid));
2489 let tag_id = layout_result.styled_dom.tag_ids_to_node_ids.iter()
2490 .find(|m| m.node_id == nid_encoded)?
2491 .tag_id
2492 .inner;
2493
2494 for item in &layout_result.display_list.items {
2497 if let DisplayListItem::HitTestArea { bounds, tag } = item {
2498 if tag.0 == tag_id && bounds.0.size.width > 0.0 && bounds.0.size.height > 0.0 {
2499 return Some(bounds.0);
2500 }
2501 }
2502 }
2503 None
2504 }
2505
2506 pub fn get_parent(&self, node_id: DomNodeId) -> Option<DomNodeId> {
2508 let layout_result = self.layout_results.get(&node_id.dom)?;
2509 let nid = node_id.node.into_crate_internal()?;
2510 let parent_id = layout_result
2511 .styled_dom
2512 .node_hierarchy
2513 .as_container()
2514 .get(nid)?
2515 .parent_id()?;
2516 Some(DomNodeId {
2517 dom: node_id.dom,
2518 node: NodeHierarchyItemId::from_crate_internal(Some(parent_id)),
2519 })
2520 }
2521
2522 pub fn get_first_child(&self, node_id: DomNodeId) -> Option<DomNodeId> {
2524 let layout_result = self.layout_results.get(&node_id.dom)?;
2525 let nid = node_id.node.into_crate_internal()?;
2526 let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
2527 let hierarchy_item = node_hierarchy.get(nid)?;
2528 let first_child_id = hierarchy_item.first_child_id(nid)?;
2529 Some(DomNodeId {
2530 dom: node_id.dom,
2531 node: NodeHierarchyItemId::from_crate_internal(Some(first_child_id)),
2532 })
2533 }
2534
2535 pub fn get_next_sibling(&self, node_id: DomNodeId) -> Option<DomNodeId> {
2537 let layout_result = self.layout_results.get(&node_id.dom)?;
2538 let nid = node_id.node.into_crate_internal()?;
2539 let next_sibling_id = layout_result
2540 .styled_dom
2541 .node_hierarchy
2542 .as_container()
2543 .get(nid)?
2544 .next_sibling_id()?;
2545 Some(DomNodeId {
2546 dom: node_id.dom,
2547 node: NodeHierarchyItemId::from_crate_internal(Some(next_sibling_id)),
2548 })
2549 }
2550
2551 pub fn get_previous_sibling(&self, node_id: DomNodeId) -> Option<DomNodeId> {
2553 let layout_result = self.layout_results.get(&node_id.dom)?;
2554 let nid = node_id.node.into_crate_internal()?;
2555 let prev_sibling_id = layout_result
2556 .styled_dom
2557 .node_hierarchy
2558 .as_container()
2559 .get(nid)?
2560 .previous_sibling_id()?;
2561 Some(DomNodeId {
2562 dom: node_id.dom,
2563 node: NodeHierarchyItemId::from_crate_internal(Some(prev_sibling_id)),
2564 })
2565 }
2566
2567 pub fn get_last_child(&self, node_id: DomNodeId) -> Option<DomNodeId> {
2569 let layout_result = self.layout_results.get(&node_id.dom)?;
2570 let nid = node_id.node.into_crate_internal()?;
2571 let last_child_id = layout_result
2572 .styled_dom
2573 .node_hierarchy
2574 .as_container()
2575 .get(nid)?
2576 .last_child_id()?;
2577 Some(DomNodeId {
2578 dom: node_id.dom,
2579 node: NodeHierarchyItemId::from_crate_internal(Some(last_child_id)),
2580 })
2581 }
2582
2583 #[allow(clippy::match_same_arms)] pub fn scan_used_fonts(&self) -> BTreeSet<FontKey> {
2591 use crate::solver3::display_list::DisplayListItem;
2592
2593 let mut fonts = BTreeSet::new();
2594 for layout_result in self.layout_results.values() {
2595 for item in &layout_result.display_list.items {
2596 let hash = match item {
2597 DisplayListItem::Text { font_hash, .. } => font_hash.font_hash,
2598 DisplayListItem::TextLayout { font_hash, .. } => font_hash.font_hash,
2599 _ => continue,
2600 };
2601 let ns = (hash >> 32) as u32;
2603 let ns = if ns == 0 { 1 } else { ns };
2604 fonts.insert(FontKey {
2605 namespace: IdNamespace(ns),
2606 key: hash,
2607 });
2608 }
2609 }
2610 fonts
2611 }
2612
2613 pub fn scan_used_images(&self, _css_image_cache: &ImageCache) -> BTreeSet<ImageRefHash> {
2619 use crate::solver3::display_list::DisplayListItem;
2620
2621 let mut images = BTreeSet::new();
2622 for layout_result in self.layout_results.values() {
2623 for item in &layout_result.display_list.items {
2624 match item {
2625 DisplayListItem::Image { image, .. } => {
2626 images.insert(image.get_hash());
2627 }
2628 DisplayListItem::PushImageMaskClip { mask_image, .. } => {
2629 images.insert(mask_image.get_hash());
2630 }
2631 _ => {}
2632 }
2633 }
2634 }
2635 images
2636 }
2637
2638 fn get_nested_scroll_states(
2640 &self,
2641 dom_id: DomId,
2642 ) -> BTreeMap<DomId, BTreeMap<NodeHierarchyItemId, ScrollPosition>> {
2643 let mut nested = BTreeMap::new();
2644 let scroll_states = self.scroll_manager.get_scroll_states_for_dom(dom_id);
2645 let mut inner = BTreeMap::new();
2646 for (node_id, scroll_pos) in scroll_states {
2647 inner.insert(
2648 NodeHierarchyItemId::from_crate_internal(Some(node_id)),
2649 scroll_pos,
2650 );
2651 }
2652 nested.insert(dom_id, inner);
2653 nested
2654 }
2655
2656 pub fn scroll_node_into_view(
2675 &mut self,
2676 node_id: DomNodeId,
2677 options: crate::managers::scroll_into_view::ScrollIntoViewOptions,
2678 now: Instant,
2679 ) -> Vec<crate::managers::scroll_into_view::ScrollAdjustment> {
2680 crate::managers::scroll_into_view::scroll_node_into_view(
2681 node_id,
2682 &self.layout_results,
2683 &mut self.scroll_manager,
2684 options,
2685 now,
2686 )
2687 }
2688
2689 pub fn scroll_cursor_into_view(
2694 &mut self,
2695 cursor_rect: LogicalRect,
2696 node_id: DomNodeId,
2697 options: crate::managers::scroll_into_view::ScrollIntoViewOptions,
2698 now: Instant,
2699 ) -> Vec<crate::managers::scroll_into_view::ScrollAdjustment> {
2700 crate::managers::scroll_into_view::scroll_cursor_into_view(
2701 cursor_rect,
2702 node_id,
2703 &self.layout_results,
2704 &mut self.scroll_manager,
2705 options,
2706 now,
2707 )
2708 }
2709
2710 pub fn add_timer(&mut self, timer_id: TimerId, timer: Timer) {
2714 self.timers.insert(timer_id, timer);
2715 }
2716
2717 pub fn remove_timer(&mut self, timer_id: &TimerId) -> Option<Timer> {
2719 self.timers.remove(timer_id)
2720 }
2721
2722 pub fn get_timer(&self, timer_id: &TimerId) -> Option<&Timer> {
2724 self.timers.get(timer_id)
2725 }
2726
2727 pub fn get_timer_mut(&mut self, timer_id: &TimerId) -> Option<&mut Timer> {
2729 self.timers.get_mut(timer_id)
2730 }
2731
2732 pub fn get_timer_ids(&self) -> TimerIdVec {
2734 self.timers.keys().copied().collect::<Vec<_>>().into()
2735 }
2736
2737 #[allow(clippy::needless_pass_by_value)]
2741 pub fn tick_timers(&mut self, current_time: Instant) -> Vec<TimerId> {
2742 let mut ready_timers = Vec::new();
2743
2744 for (timer_id, timer) in &mut self.timers {
2745 ready_timers.push(*timer_id);
2750 }
2751
2752 ready_timers
2753 }
2754
2755 pub fn time_until_next_timer_ms(
2770 &self,
2771 get_system_time_fn: &azul_core::task::GetSystemTimeCallback,
2772 ) -> Option<u64> {
2773 if self.timers.is_empty() {
2774 return None; }
2776
2777 let now = (get_system_time_fn.cb)();
2778 let mut min_ms: Option<u64> = None;
2779
2780 for timer in self.timers.values() {
2781 let next_run = timer.instant_of_next_run();
2782
2783 let ms_until = if next_run < now {
2785 0 } else {
2787 duration_to_millis(next_run.duration_since(&now))
2788 };
2789
2790 min_ms = Some(min_ms.map_or(ms_until, |current_min| current_min.min(ms_until)));
2791 }
2792
2793 min_ms
2794 }
2795
2796 pub fn add_thread(&mut self, thread_id: ThreadId, thread: Thread) {
2800 self.threads.insert(thread_id, thread);
2801 }
2802
2803 pub fn remove_thread(&mut self, thread_id: &ThreadId) -> Option<Thread> {
2805 self.threads.remove(thread_id)
2806 }
2807
2808 pub fn get_thread(&self, thread_id: &ThreadId) -> Option<&Thread> {
2810 self.threads.get(thread_id)
2811 }
2812
2813 pub fn get_thread_mut(&mut self, thread_id: &ThreadId) -> Option<&mut Thread> {
2815 self.threads.get_mut(thread_id)
2816 }
2817
2818 pub fn get_thread_ids(&self) -> ThreadIdVec {
2820 self.threads.keys().copied().collect::<Vec<_>>().into()
2821 }
2822
2823 pub fn create_cursor_blink_timer(&self, _window_state: &FullWindowState) -> Timer {
2837 use crate::timer::{Timer, TimerCallback};
2838 use azul_core::refany::RefAny;
2839
2840 let interval = self.text_edit_manager.blink.blink_interval;
2841
2842 let refany = RefAny::new(());
2845
2846 Timer {
2847 refany,
2848 node_id: None.into(),
2849 created: Instant::now(),
2850 run_count: 0,
2851 last_run: azul_core::task::OptionInstant::None,
2852 delay: azul_core::task::OptionDuration::None,
2853 interval: azul_core::task::OptionDuration::Some(interval),
2854 timeout: azul_core::task::OptionDuration::None,
2855 callback: TimerCallback::create(cursor_blink_timer_callback),
2856 }
2857 }
2858
2859 pub fn create_tooltip_delay_timer(&self, hover_time_ms: u32) -> Timer {
2867 use azul_core::task::{Duration, SystemTimeDiff};
2868 use crate::timer::{Timer, TimerCallback};
2869 use azul_core::refany::RefAny;
2870
2871 Timer {
2872 refany: RefAny::new(()),
2873 node_id: None.into(),
2874 created: Instant::now(),
2875 run_count: 0,
2876 last_run: azul_core::task::OptionInstant::None,
2877 delay: azul_core::task::OptionDuration::Some(Duration::System(
2878 SystemTimeDiff::from_millis(u64::from(hover_time_ms)),
2879 )),
2880 interval: azul_core::task::OptionDuration::None,
2881 timeout: azul_core::task::OptionDuration::None,
2882 callback: TimerCallback::create(tooltip_delay_timer_callback),
2883 }
2884 }
2885
2886 pub fn handle_hover_change_for_tooltip(&self, hover_time_ms: u32) -> TooltipTimerAction {
2901 let current_hover = self.hover_manager.current_hover_node();
2902 let previous_hover = self.hover_manager.previous_hover_node();
2903
2904 if current_hover == previous_hover {
2905 return TooltipTimerAction::NoChange;
2906 }
2907
2908 let dom_id = DomId { inner: 0 };
2909 let Some(layout_result) = self.layout_results.get(&dom_id) else {
2910 return TooltipTimerAction::Stop;
2911 };
2912 let node_data_cont = layout_result.styled_dom.node_data.as_container();
2913
2914 let node_has_tooltip = |node_id: NodeId| -> bool {
2915 node_data_cont
2916 .get(node_id)
2917 .is_some_and(|n| n.get_accessible_label().is_some())
2918 };
2919
2920 match current_hover {
2921 Some(node) if node_has_tooltip(node) => {
2922 TooltipTimerAction::Start(self.create_tooltip_delay_timer(hover_time_ms))
2923 }
2924 _ => TooltipTimerAction::Stop,
2925 }
2926 }
2927
2928 fn is_node_contenteditable_internal(&self, dom_id: DomId, node_id: NodeId) -> bool {
2930 use crate::solver3::getters::is_node_contenteditable;
2931
2932 let Some(layout_result) = self.layout_results.get(&dom_id) else {
2933 return false;
2934 };
2935
2936 is_node_contenteditable(&layout_result.styled_dom, node_id)
2937 }
2938
2939 fn is_node_contenteditable_inherited_internal(&self, dom_id: DomId, node_id: NodeId) -> bool {
2945 use crate::solver3::getters::is_node_contenteditable_inherited;
2946
2947 let Some(layout_result) = self.layout_results.get(&dom_id) else {
2948 return false;
2949 };
2950
2951 is_node_contenteditable_inherited(&layout_result.styled_dom, node_id)
2952 }
2953
2954 #[must_use]
2967 pub fn caret_blink_interval_for(&self, dom_id: DomId, node_id: NodeId) -> Duration {
2968 use crate::{managers::text_edit::CURSOR_BLINK_INTERVAL, solver3::getters::get_caret_style};
2969
2970 let Some(layout_result) = self.layout_results.get(&dom_id) else {
2971 return CURSOR_BLINK_INTERVAL;
2972 };
2973
2974 let interval: Duration = get_caret_style(&layout_result.styled_dom, Some(node_id))
2975 .animation_duration
2976 .into();
2977
2978 if interval.as_nanos() == 0 {
2979 CURSOR_BLINK_INTERVAL
2980 } else {
2981 interval
2982 }
2983 }
2984
2985 pub fn handle_focus_change_for_cursor_blink(
3005 &mut self,
3006 new_focus: Option<DomNodeId>,
3007 current_window_state: &FullWindowState,
3008 ) -> CursorBlinkTimerAction {
3009 let contenteditable_info = new_focus.and_then(|focus_node| {
3012 focus_node.node.into_crate_internal().and_then(|node_id| {
3013 if self.is_node_contenteditable_inherited_internal(focus_node.dom, node_id) {
3015 let text_node_id = self.find_last_text_child(focus_node.dom, node_id)
3017 .unwrap_or(node_id);
3018 Some((focus_node.dom, node_id, text_node_id))
3019 } else {
3020 None
3021 }
3022 })
3023 });
3024
3025 let timer_was_active = self.text_edit_manager.blink.is_blink_timer_active();
3027
3028 if let Some((dom_id, container_node_id, text_node_id)) = contenteditable_info {
3029
3030 self.focus_manager.set_pending_contenteditable_focus(
3033 dom_id,
3034 container_node_id,
3035 text_node_id,
3036 );
3037
3038 let blink_interval = self.caret_blink_interval_for(dom_id, container_node_id);
3047 self.text_edit_manager
3048 .blink
3049 .set_blink_interval(blink_interval);
3050
3051 let now = Instant::now();
3053 self.text_edit_manager.blink.reset_blink_on_input(now);
3054 self.text_edit_manager.blink.set_blink_timer_active(true);
3055
3056 if timer_was_active {
3057 CursorBlinkTimerAction::NoChange
3059 } else {
3060 let timer = self.create_cursor_blink_timer(current_window_state);
3062 CursorBlinkTimerAction::Start(timer)
3063 }
3064 } else {
3065 self.text_edit_manager.clear_editing();
3069 self.focus_manager.clear_pending_contenteditable_focus();
3070
3071 if timer_was_active {
3072 self.text_edit_manager.blink.set_blink_timer_active(false);
3074 CursorBlinkTimerAction::Stop
3075 } else {
3076 CursorBlinkTimerAction::NoChange
3077 }
3078 }
3079 }
3080
3081 pub fn finalize_pending_focus_changes(&mut self) -> bool {
3103 let Some(pending) = self.focus_manager.take_pending_contenteditable_focus() else {
3105 return false;
3106 };
3107
3108 if self.text_edit_manager.multi_cursor.as_ref().is_some_and(|mc| mc.node_id.dom == pending.dom_id && mc.node_id.node.into_crate_internal() == Some(pending.text_node_id))
3113 || self.text_edit_manager.multi_cursor.as_ref().is_some_and(|mc| mc.node_id.dom == pending.dom_id && mc.node_id.node.into_crate_internal() == Some(pending.container_node_id))
3114 {
3115 return true;
3116 }
3117
3118 let text_layout = self.get_inline_layout_for_node(pending.dom_id, pending.text_node_id).cloned();
3120
3121 let cursor = text_layout.as_ref()
3124 .and_then(|layout| {
3125 layout.items.iter().rev()
3126 .find_map(|item| if let ShapedItem::Cluster(c) = &item.item {
3127 Some(TextCursor {
3128 cluster_id: c.source_cluster_id,
3129 affinity: CursorAffinity::Trailing,
3130 })
3131 } else { None })
3132 })
3133 .unwrap_or(TextCursor {
3134 cluster_id: GraphemeClusterId { source_run: 0, start_byte_in_run: 0 },
3135 affinity: CursorAffinity::Trailing,
3136 });
3137 self.text_edit_manager.initialize_editing(cursor, pending.dom_id, pending.text_node_id, 0);
3138 true
3139 }
3140
3141 pub fn get_inline_layout_for_node(
3151 &self,
3152 dom_id: DomId,
3153 node_id: NodeId,
3154 ) -> Option<&Arc<UnifiedLayout>> {
3155 let layout_result = self.layout_results.get(&dom_id)?;
3156
3157 let layout_indices = layout_result.layout_tree.dom_to_layout.get(&node_id)?;
3158 let layout_index = *layout_indices.first()?;
3159
3160 layout_result.layout_tree.get_inline_layout_for_node(layout_index)
3162 }
3163
3164 fn resolve_step_static(
3166 layout: &UnifiedLayout,
3167 cursor: &TextCursor,
3168 direction: azul_core::events::SelectionDirection,
3169 step: azul_core::events::SelectionStep,
3170 ) -> TextCursor {
3171 use azul_core::events::{SelectionDirection as D, SelectionStep as S};
3172 match (direction, step) {
3173 (D::Backward, S::Character) => layout.move_cursor_left(*cursor, &mut None),
3174 (D::Forward, S::Character) => layout.move_cursor_right(*cursor, &mut None),
3175 (D::Backward, S::Word) => layout.move_cursor_to_prev_word(*cursor, &mut None),
3176 (D::Forward, S::Word) => layout.move_cursor_to_next_word(*cursor, &mut None),
3177 (D::Backward, S::VisualLine) => layout.move_cursor_up(*cursor, &mut None, &mut None),
3178 (D::Forward, S::VisualLine) => layout.move_cursor_down(*cursor, &mut None, &mut None),
3179 (D::Backward, S::Line) => layout.move_cursor_to_line_start(*cursor, &mut None),
3180 (D::Forward, S::Line) => layout.move_cursor_to_line_end(*cursor, &mut None),
3181 (D::Backward, S::Document) => layout.get_first_cluster_cursor().unwrap_or(*cursor),
3182 (D::Forward, S::Document) => layout.get_last_cluster_cursor().unwrap_or(*cursor),
3183 }
3184 }
3185
3186 pub fn apply_selection_op(
3192 &mut self,
3193 target: DomNodeId,
3194 op: &azul_core::events::SelectionOp,
3195 ) -> bool {
3196 use azul_core::events::{SelectionMode, SelectionStep, SelectionDirection};
3197
3198 let dom_id = target.dom;
3199 let Some(node_id) = target.node.into_crate_internal() else {
3200 return false;
3201 };
3202
3203 let layout = match self.get_inline_layout_for_node(dom_id, node_id) {
3204 Some(l) => l.clone(),
3205 None => return false,
3206 };
3207
3208 match op.mode {
3209 SelectionMode::Move | SelectionMode::Extend => {
3210 let extend = matches!(op.mode, SelectionMode::Extend);
3211 if let Some(ref mut mc) = self.text_edit_manager.multi_cursor {
3212 for _ in 0..op.repeat.max(1) {
3213 mc.move_all_cursors(extend, |c| {
3214 Self::resolve_step_static(&layout, c, op.direction, op.step)
3215 });
3216 }
3217 }
3218 self.regenerate_display_list_for_dom(dom_id);
3219 true
3220 }
3221 SelectionMode::Delete => {
3222 if !matches!(op.step, SelectionStep::Character) {
3224 if let Some(ref mut mc) = self.text_edit_manager.multi_cursor {
3225 for _ in 0..op.repeat.max(1) {
3226 mc.move_all_cursors(true, |c| {
3227 Self::resolve_step_static(&layout, c, op.direction, op.step)
3228 });
3229 }
3230 }
3231 }
3232 let forward = matches!(op.direction, SelectionDirection::Forward);
3234 self.delete_selection(target, forward).is_some()
3235 }
3236 }
3237 }
3238
3239 pub fn move_cursor_in_node<F>(
3241 &self,
3242 dom_id: DomId,
3243 node_id: NodeId,
3244 movement_fn: F,
3245 ) -> Option<TextCursor>
3246 where
3247 F: FnOnce(&UnifiedLayout, &TextCursor) -> TextCursor,
3248 {
3249 let current_cursor = self.text_edit_manager.get_primary_cursor()?;
3250 let layout = self.get_inline_layout_for_node(dom_id, node_id)?;
3251
3252 let new_cursor = movement_fn(layout, ¤t_cursor);
3253
3254 if new_cursor == current_cursor {
3256 None
3257 } else {
3258 Some(new_cursor)
3259 }
3260 }
3261
3262 pub fn handle_cursor_movement(
3267 &mut self,
3268 dom_id: DomId,
3269 node_id: NodeId,
3270 new_cursor: TextCursor,
3271 extend_selection: bool,
3272 ) {
3273 if let Some(ref mut mc) = self.text_edit_manager.multi_cursor {
3275 mc.set_single_cursor(new_cursor);
3276 }
3277
3278 self.regenerate_display_list_for_dom(dom_id);
3279 }
3280
3281 pub fn handle_multi_cursor_movement(
3284 &mut self,
3285 dom_id: DomId,
3286 node_id: NodeId,
3287 extend_selection: bool,
3288 move_fn: impl Fn(&TextCursor) -> TextCursor,
3289 ) {
3290 if let Some(ref mut mc) = self.text_edit_manager.multi_cursor {
3291 mc.move_all_cursors(extend_selection, &move_fn);
3292 } else {
3293 if let Some(cursor) = self.text_edit_manager.get_primary_cursor() {
3295 let new_cursor = move_fn(&cursor);
3296 self.handle_cursor_movement(dom_id, node_id, new_cursor, extend_selection);
3297 return;
3298 }
3299 }
3300
3301 self.regenerate_display_list_for_dom(dom_id);
3302 }
3303
3304 pub fn get_gpu_cache(&self, dom_id: &DomId) -> Option<&GpuValueCache> {
3308 self.gpu_state_manager.caches.get(dom_id)
3309 }
3310
3311 pub fn get_gpu_cache_mut(&mut self, dom_id: &DomId) -> Option<&mut GpuValueCache> {
3313 self.gpu_state_manager.caches.get_mut(dom_id)
3314 }
3315
3316 pub fn get_or_create_gpu_cache(&mut self, dom_id: DomId) -> &mut GpuValueCache {
3318 self.gpu_state_manager.get_or_create_cache(dom_id)
3319 }
3320
3321 pub fn get_layout_result(&self, dom_id: &DomId) -> Option<&DomLayoutResult> {
3325 self.layout_results.get(dom_id)
3326 }
3327
3328 pub fn get_layout_result_mut(&mut self, dom_id: &DomId) -> Option<&mut DomLayoutResult> {
3330 self.layout_results.get_mut(dom_id)
3331 }
3332
3333 pub fn get_dom_ids(&self) -> DomIdVec {
3335 self.layout_results
3336 .keys()
3337 .copied()
3338 .collect::<Vec<_>>()
3339 .into()
3340 }
3341
3342 pub fn compute_cursor_type_hit_test(
3349 &self,
3350 hit_test: &crate::hit_test::FullHitTest,
3351 ) -> crate::hit_test::CursorTypeHitTest {
3352 crate::hit_test::CursorTypeHitTest::new(hit_test, self)
3353 }
3354
3355 #[allow(clippy::needless_pass_by_value)]
3358 fn calculate_scrollbar_opacity(
3359 last_activity: Option<Instant>,
3360 now: Instant,
3361 fade_delay: Duration,
3362 fade_duration: Duration,
3363 ) -> f32 {
3364 let Some(last_activity) = last_activity else {
3365 return 0.0;
3366 };
3367
3368 let time_since_activity = now.duration_since(&last_activity);
3369
3370 if time_since_activity.div(&fade_delay) < 1.0 {
3372 return 1.0;
3373 }
3374
3375 let time_into_fade = time_since_activity.div(&fade_delay) - 1.0;
3377 let fade_progress = (time_into_fade * fade_delay.div(&fade_duration)).min(1.0);
3378
3379 (1.0 - fade_progress).max(0.0)
3381 }
3382
3383 #[allow(clippy::too_many_lines)] #[cfg(feature = "std")]
3423 pub fn refresh_scrollbar_gpu_cache_for_cpu_frame(&mut self) -> bool {
3424 let system_callbacks = ExternalSystemCallbacks::rust_internal();
3425 let mut moved = false;
3426 {
3427 let Self {
3428 ref layout_results,
3429 ref scroll_manager,
3430 ref mut gpu_state_manager,
3431 ..
3432 } = *self;
3433 for (dom_id, layout_result) in layout_results {
3434 moved |= !gpu_state_manager
3435 .update_scrollbar_transforms(
3436 *dom_id,
3437 scroll_manager,
3438 &layout_result.layout_tree,
3439 )
3440 .is_empty();
3441 }
3442 }
3443 let fade_delay = self.gpu_state_manager.fade_delay;
3444 let fade_duration = self.gpu_state_manager.fade_duration;
3445 let Self {
3446 ref layout_results,
3447 ref scroll_manager,
3448 ref mut gpu_state_manager,
3449 ..
3450 } = *self;
3451 for (dom_id, layout_result) in layout_results {
3452 moved |= !Self::synchronize_scrollbar_opacity(
3453 gpu_state_manager,
3454 scroll_manager,
3455 *dom_id,
3456 &layout_result.layout_tree,
3457 &system_callbacks,
3458 fade_delay,
3459 fade_duration,
3460 )
3461 .is_empty();
3462 }
3463 moved
3464 }
3465
3466 #[allow(clippy::too_many_lines)] pub fn synchronize_scrollbar_opacity(
3468 gpu_state_manager: &mut GpuStateManager,
3469 scroll_manager: &ScrollManager,
3470 dom_id: DomId,
3471 layout_tree: &LayoutTree,
3472 system_callbacks: &ExternalSystemCallbacks,
3473 fade_delay: Duration,
3474 fade_duration: Duration,
3475 ) -> Vec<GpuScrollbarOpacityEvent> {
3476 let mut events = Vec::new();
3477 let mut any_opacity_nonzero = false;
3478 let gpu_cache = gpu_state_manager.caches.entry(dom_id).or_default();
3479
3480 let now = (system_callbacks.get_system_time_fn.cb)();
3482
3483 for (node_idx, node) in layout_tree.nodes.iter().enumerate() {
3485 let warm = layout_tree.warm(node_idx);
3487 let Some(scrollbar_info) = warm.and_then(|w| w.scrollbar_info.as_ref()) else {
3488 continue;
3489 };
3490
3491 let Some(node_id) = node.dom_node_id else {
3492 continue; };
3494
3495 let vertical_opacity = if scrollbar_info.needs_vertical {
3497 Self::calculate_scrollbar_opacity(
3498 scroll_manager.get_last_activity_time(dom_id, node_id),
3499 now.clone(),
3500 fade_delay,
3501 fade_duration,
3502 )
3503 } else {
3504 0.0
3505 };
3506
3507 let horizontal_opacity = if scrollbar_info.needs_horizontal {
3508 Self::calculate_scrollbar_opacity(
3509 scroll_manager.get_last_activity_time(dom_id, node_id),
3510 now.clone(),
3511 fade_delay,
3512 fade_duration,
3513 )
3514 } else {
3515 0.0
3516 };
3517
3518 if (vertical_opacity > 0.0 && vertical_opacity < 1.0)
3524 || (horizontal_opacity > 0.0 && horizontal_opacity < 1.0)
3525 {
3526 any_opacity_nonzero = true;
3527 }
3528
3529 let key = (dom_id, node_id);
3538 if scrollbar_info.needs_vertical {
3539 let existing = gpu_cache.scrollbar_v_opacity_values.get(&key);
3540
3541 match existing {
3542 None => {
3543 let opacity_key = OpacityKey::unique();
3544 gpu_cache.scrollbar_v_opacity_keys.insert(key, opacity_key);
3545 gpu_cache
3546 .scrollbar_v_opacity_values
3547 .insert(key, vertical_opacity);
3548 events.push(GpuScrollbarOpacityEvent::VerticalAdded(
3549 dom_id,
3550 node_id,
3551 opacity_key,
3552 vertical_opacity,
3553 ));
3554 }
3555 Some(&old_opacity) if (old_opacity - vertical_opacity).abs() > 0.001 => {
3556 let opacity_key = gpu_cache.scrollbar_v_opacity_keys[&key];
3557 gpu_cache
3558 .scrollbar_v_opacity_values
3559 .insert(key, vertical_opacity);
3560 events.push(GpuScrollbarOpacityEvent::VerticalChanged(
3561 dom_id,
3562 node_id,
3563 opacity_key,
3564 old_opacity,
3565 vertical_opacity,
3566 ));
3567 }
3568 _ => {}
3569 }
3570 } else {
3571 if let Some(opacity_key) = gpu_cache.scrollbar_v_opacity_keys.remove(&key) {
3573 gpu_cache.scrollbar_v_opacity_values.remove(&key);
3574 events.push(GpuScrollbarOpacityEvent::VerticalRemoved(
3575 dom_id,
3576 node_id,
3577 opacity_key,
3578 ));
3579 }
3580 }
3581
3582 if scrollbar_info.needs_horizontal {
3584 let existing = gpu_cache.scrollbar_h_opacity_values.get(&key);
3585
3586 match existing {
3587 None => {
3588 let opacity_key = OpacityKey::unique();
3589 gpu_cache.scrollbar_h_opacity_keys.insert(key, opacity_key);
3590 gpu_cache
3591 .scrollbar_h_opacity_values
3592 .insert(key, horizontal_opacity);
3593 events.push(GpuScrollbarOpacityEvent::HorizontalAdded(
3594 dom_id,
3595 node_id,
3596 opacity_key,
3597 horizontal_opacity,
3598 ));
3599 }
3600 Some(&old_opacity) if (old_opacity - horizontal_opacity).abs() > 0.001 => {
3601 let opacity_key = gpu_cache.scrollbar_h_opacity_keys[&key];
3602 gpu_cache
3603 .scrollbar_h_opacity_values
3604 .insert(key, horizontal_opacity);
3605 events.push(GpuScrollbarOpacityEvent::HorizontalChanged(
3606 dom_id,
3607 node_id,
3608 opacity_key,
3609 old_opacity,
3610 horizontal_opacity,
3611 ));
3612 }
3613 _ => {}
3614 }
3615 } else {
3616 if let Some(opacity_key) = gpu_cache.scrollbar_h_opacity_keys.remove(&key) {
3618 gpu_cache.scrollbar_h_opacity_values.remove(&key);
3619 events.push(GpuScrollbarOpacityEvent::HorizontalRemoved(
3620 dom_id,
3621 node_id,
3622 opacity_key,
3623 ));
3624 }
3625 }
3626 }
3627
3628 gpu_state_manager.scrollbar_fade_active = any_opacity_nonzero;
3632
3633 events
3634 }
3635
3636 #[must_use] pub fn compute_scroll_ids(
3645 layout_tree: &LayoutTree,
3646 styled_dom: &StyledDom,
3647 ) -> (HashMap<usize, u64>, HashMap<u64, NodeId>) {
3648 use azul_css::props::layout::LayoutOverflow;
3649
3650 use crate::solver3::getters::{get_overflow_x, get_overflow_y};
3651
3652 let mut scroll_ids = HashMap::new();
3653 let mut scroll_id_to_node_id = HashMap::new();
3654
3655 for (layout_idx, node) in layout_tree.nodes.iter().enumerate() {
3657 let Some(dom_node_id) = node.dom_node_id else {
3658 continue;
3659 };
3660
3661 let styled_node_state = styled_dom
3663 .styled_nodes
3664 .as_container()
3665 .get(dom_node_id)
3666 .map(|n| n.styled_node_state)
3667 .unwrap_or_default();
3668
3669 let overflow_x = get_overflow_x(styled_dom, dom_node_id, &styled_node_state);
3671 let overflow_y = get_overflow_y(styled_dom, dom_node_id, &styled_node_state);
3672
3673 let is_scrollable = overflow_x.is_scroll() || overflow_y.is_scroll();
3674
3675 if !is_scrollable {
3676 continue;
3677 }
3678
3679 let scroll_id = {
3682 use std::hash::{Hash, Hasher, DefaultHasher};
3683 let mut h = DefaultHasher::new();
3684 if let Some(cold) = layout_tree.cold(layout_idx) {
3685 cold.node_data_fingerprint.hash(&mut h);
3686 }
3687 h.finish()
3688 };
3689
3690 scroll_ids.insert(layout_idx, scroll_id);
3691 scroll_id_to_node_id.insert(scroll_id, dom_node_id);
3692 }
3693
3694 (scroll_ids, scroll_id_to_node_id)
3695 }
3696
3697 #[allow(clippy::cast_possible_truncation)] pub fn get_node_layout_rect(
3705 &self,
3706 node_id: DomNodeId,
3707 ) -> Option<LogicalRect> {
3708 let layout_tree = self.layout_cache.tree.as_ref()?;
3710 { let _ = (0xE5_000002u32 | ((layout_tree.nodes.len() as u32 & 0xff) << 8)); }
3711
3712 let target_node_id = node_id.node.into_crate_internal();
3715 let Some(layout_idx) = layout_tree.nodes.iter().position(|node| node.dom_node_id == target_node_id) else { { let _ = (0xE5_0000FFu32); } return None; };
3716 { let _ = (0xE5_000003u32 | ((self.layout_cache.calculated_positions.len() as u32 & 0xfff) << 8)); }
3717
3718 let Some(calc_pos) = self.layout_cache.calculated_positions.get(layout_idx) else { { let _ = (0xE5_0000FEu32); } return None; };
3720
3721 let layout_node = layout_tree.nodes.get(layout_idx)?;
3723
3724 let Some(used_size) = layout_node.used_size else { { let _ = (0xE5_0000FDu32); } return None; };
3726 { let _ = (0xE5_000004u32); }
3727
3728 let hidpi_factor = self
3730 .current_window_state
3731 .size
3732 .get_hidpi_factor()
3733 .inner
3734 .get();
3735
3736 Some(LogicalRect::new(
3737 LogicalPosition::new(calc_pos.x, calc_pos.y),
3738 LogicalSize::new(
3739 used_size.width / hidpi_factor,
3740 used_size.height / hidpi_factor,
3741 ),
3742 ))
3743 }
3744
3745 #[cfg(feature = "a11y")]
3764 pub fn update_a11y_tree(&mut self) {
3765 let cursor_a11y_info = self.text_edit_manager.multi_cursor.as_ref().and_then(|mc| {
3766 let node_id = mc.node_id.node.into_crate_internal()?;
3767 let primary = mc.get_primary()?;
3768 let (anchor_offset, focus_offset) = match &primary.selection {
3769 Selection::Cursor(c) => {
3770 let off = c.cluster_id.start_byte_in_run as usize;
3771 (off, off)
3772 }
3773 Selection::Range(r) => (
3774 r.start.cluster_id.start_byte_in_run as usize,
3775 r.end.cluster_id.start_byte_in_run as usize,
3776 ),
3777 };
3778 Some(crate::managers::a11y::CursorA11yInfo {
3779 dom_id: mc.node_id.dom,
3780 node_id,
3781 anchor_offset,
3782 focus_offset,
3783 })
3784 });
3785
3786 let mut dirty_text_overrides: BTreeMap<(DomId, NodeId), String> = BTreeMap::new();
3789 for (&(dom_id, node_id), dirty_node) in &self.dirty_text_nodes {
3790 dirty_text_overrides.insert(
3791 (dom_id, node_id),
3792 self.extract_text_from_inline_content(&dirty_node.content),
3793 );
3794 }
3795
3796 let a11y_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3797 crate::managers::a11y::A11yManager::update_tree(
3798 self.a11y_manager.root_id,
3799 &self.layout_results,
3800 &self.scroll_manager,
3801 &self.current_window_state.title,
3802 self.current_window_state.size.dimensions,
3803 self.focus_manager.get_focused_node().copied(),
3804 self.current_window_state.size.get_hidpi_factor().inner.get(),
3805 &dirty_text_overrides,
3806 cursor_a11y_info,
3807 )
3808 }));
3809
3810 if let Ok(tree_update) = a11y_result {
3811 self.a11y_manager.last_tree_update = Some(tree_update);
3812 self.a11y_manager.tree_initialized = true;
3813 }
3814 }
3815
3816 #[cfg(feature = "a11y")]
3828 #[must_use]
3829 pub fn build_a11y_snapshot(&self) -> crate::managers::a11y_snapshot::A11ySnapshot {
3830 crate::managers::a11y_snapshot::A11ySnapshot::build(
3831 &self.layout_results,
3832 &self.scroll_manager,
3833 self.focus_manager.get_focused_node().copied(),
3834 self.current_window_state.title.as_str(),
3835 self.current_window_state.size.dimensions,
3836 )
3837 }
3838
3839 #[cfg(feature = "a11y")]
3843 #[allow(clippy::cast_possible_truncation)] pub fn update_a11y_tree_incremental(&mut self) {
3845 if !self.a11y_manager.tree_initialized {
3846 return self.update_a11y_tree();
3848 }
3849
3850 let Some(mc) = self.text_edit_manager.multi_cursor.as_ref() else {
3852 return; };
3854
3855 let dom_node_id = mc.node_id;
3856 let Some(node_id) = dom_node_id.node.into_crate_internal() else {
3857 return;
3858 };
3859 let dom_id = dom_node_id.dom;
3860
3861 let text_content = if let Some(dirty) = self.dirty_text_nodes.get(&(dom_id, node_id)) {
3863 self.extract_text_from_inline_content(&dirty.content)
3864 } else {
3865 let Some(lr) = self.layout_results.get(&dom_id) else {
3867 return self.update_a11y_tree();
3868 };
3869 let node_data = lr.styled_dom.node_data.as_ref();
3870 let hierarchy = lr.styled_dom.node_hierarchy.as_ref();
3871 let mut text = String::new();
3872 if let Some(item) = hierarchy.get(node_id.index()) {
3873 let mut child = item.first_child_id(node_id);
3874 while let Some(child_id) = child {
3875 if let Some(cd) = node_data.get(child_id.index()) {
3876 if let NodeType::Text(t) = &cd.node_type {
3877 if !text.is_empty() { text.push(' '); }
3878 text.push_str(t.as_str());
3879 }
3880 }
3881 if child_id.index() >= hierarchy.len() { break; }
3882 child = hierarchy[child_id.index()].next_sibling_id();
3883 }
3884 }
3885 text
3886 };
3887
3888 let a11y_node_id = accesskit::NodeId(
3890 ((dom_id.inner as u64) << 32) | ((node_id.index() as u64) + 1),
3891 );
3892
3893 let role = self.layout_results.get(&dom_id)
3895 .and_then(|lr| lr.styled_dom.node_data.as_ref().get(node_id.index()))
3896 .map_or(accesskit::Role::GenericContainer, |nd| {
3897 if nd.is_contenteditable() || matches!(nd.node_type, NodeType::TextArea) {
3898 accesskit::Role::MultilineTextInput
3899 } else if matches!(nd.node_type, NodeType::Input) {
3900 accesskit::Role::TextInput
3901 } else {
3902 accesskit::Role::GenericContainer
3903 }
3904 });
3905
3906 let mut node = accesskit::Node::new(role);
3907 node.set_value(text_content.as_str());
3908 node.add_action(accesskit::Action::SetTextSelection);
3909 node.add_action(accesskit::Action::ReplaceSelectedText);
3910 node.add_action(accesskit::Action::SetValue);
3911
3912 let primary = mc.get_primary();
3914 if let Some(identified) = primary {
3915 let (anchor_off, focus_off) = match &identified.selection {
3916 Selection::Cursor(c) => {
3917 let off = c.cluster_id.start_byte_in_run as usize;
3918 (off, off)
3919 }
3920 Selection::Range(r) => (
3921 r.start.cluster_id.start_byte_in_run as usize,
3922 r.end.cluster_id.start_byte_in_run as usize,
3923 ),
3924 };
3925
3926 let char_lengths: Vec<u8> = text_content.chars()
3927 .map(|c| c.len_utf16() as u8)
3928 .collect();
3929 node.set_character_lengths(char_lengths.clone());
3930
3931 let byte_to_char = |byte_off: usize| -> usize {
3932 text_content.char_indices()
3933 .take_while(|(b, _)| *b < byte_off)
3934 .count()
3935 .min(char_lengths.len())
3936 };
3937
3938 node.set_text_selection(accesskit::TextSelection {
3939 anchor: accesskit::TextPosition {
3940 node: a11y_node_id,
3941 character_index: byte_to_char(anchor_off),
3942 },
3943 focus: accesskit::TextPosition {
3944 node: a11y_node_id,
3945 character_index: byte_to_char(focus_off),
3946 },
3947 });
3948 }
3949
3950 let focus = self.focus_manager.get_focused_node().copied()
3952 .and_then(|dn| {
3953 let idx = dn.node.into_crate_internal()?.index();
3954 Some(accesskit::NodeId(((dn.dom.inner as u64) << 32) | ((idx as u64) + 1)))
3955 })
3956 .unwrap_or(self.a11y_manager.root_id);
3957
3958 self.a11y_manager.last_tree_update = Some(accesskit::TreeUpdate {
3959 nodes: vec![(a11y_node_id, node)],
3960 tree: None, focus,
3962 tree_id: accesskit::TreeId::ROOT,
3963 });
3964 }
3965
3966 pub fn get_focused_cursor_rect(&self) -> Option<LogicalRect> {
3967 let focused_node = self.focus_manager.focused_node?;
3969
3970 let cursor = self.text_edit_manager.get_primary_cursor()?;
3972
3973 let layout_tree = self.layout_cache.tree.as_ref()?;
3975
3976 let target_node_id = focused_node.node.into_crate_internal();
3978 let layout_idx = layout_tree
3979 .nodes
3980 .iter()
3981 .position(|node| node.dom_node_id == target_node_id)?;
3982
3983 let warm_node = layout_tree.warm(layout_idx)?;
3985 let cached_layout = warm_node.inline_layout_result.as_ref()?;
3986 let inline_layout = &cached_layout.layout;
3987
3988 let mut cursor_rect = inline_layout.get_cursor_rect(&cursor)?;
3990
3991 let calc_pos = self.layout_cache.calculated_positions.get(layout_idx)?;
3993
3994 cursor_rect.origin.x += calc_pos.x;
3996 cursor_rect.origin.y += calc_pos.y;
3997
3998 Some(cursor_rect)
4000 }
4001
4002 pub fn calculate_selection_bounding_rect(&self) -> Option<LogicalRect> {
4005 let focused_node = self.focus_manager.focused_node?;
4006 let mc = self.text_edit_manager.multi_cursor.as_ref()?;
4007
4008 let ranges: Vec<_> = mc.selections.iter().filter_map(|s| {
4010 if let Selection::Range(ref r) = s.selection {
4011 Some(*r)
4012 } else {
4013 None
4014 }
4015 }).collect();
4016
4017 if ranges.is_empty() {
4018 return None;
4019 }
4020
4021 let target_node_id = focused_node.node.into_crate_internal();
4023 let layout_tree = self.layout_cache.tree.as_ref()?;
4024 let layout_idx = layout_tree.nodes.iter()
4025 .position(|n| n.dom_node_id == target_node_id)?;
4026 let warm = layout_tree.warm(layout_idx)?;
4027 let inline_layout = &warm.inline_layout_result.as_ref()?.layout;
4028 let calc_pos = self.layout_cache.calculated_positions.get(layout_idx)?;
4029
4030 let mut min_x = f32::MAX;
4031 let mut min_y = f32::MAX;
4032 let mut max_x = f32::MIN;
4033 let mut max_y = f32::MIN;
4034 let mut found_any = false;
4035
4036 for range in &ranges {
4037 for rect in inline_layout.get_selection_rects(range) {
4038 found_any = true;
4039 let abs_x = rect.origin.x + calc_pos.x;
4040 let abs_y = rect.origin.y + calc_pos.y;
4041 min_x = min_x.min(abs_x);
4042 min_y = min_y.min(abs_y);
4043 max_x = max_x.max(abs_x + rect.size.width);
4044 max_y = max_y.max(abs_y + rect.size.height);
4045 }
4046 }
4047
4048 if !found_any {
4049 return None;
4050 }
4051
4052 Some(LogicalRect::new(
4053 LogicalPosition { x: min_x, y: min_y },
4054 LogicalSize { width: max_x - min_x, height: max_y - min_y },
4055 ))
4056 }
4057
4058 #[allow(clippy::cast_possible_truncation)] #[allow(clippy::too_many_lines)] pub fn select_next_occurrence(&mut self) -> bool {
4071 use crate::text3::selection::select_word_at_cursor;
4072
4073 let Some(mc) = self.text_edit_manager.multi_cursor.as_mut() else {
4074 return false;
4075 };
4076 let node_id = mc.node_id;
4077 let Some(dom_node_id) = node_id.node.into_crate_internal() else {
4078 return false;
4079 };
4080
4081 let primary = match mc.selections.first() {
4083 Some(s) => *s,
4084 None => return false,
4085 };
4086
4087 let (search_range, need_word_expand) = match &primary.selection {
4088 Selection::Range(r) => (*r, false),
4089 Selection::Cursor(c) => {
4090 (SelectionRange { start: *c, end: *c }, true)
4092 }
4093 };
4094
4095 let Some(inline_layout) = self.get_node_inline_layout(node_id.dom, dom_node_id) else {
4097 return false;
4098 };
4099
4100 let word_range = if need_word_expand {
4102 match select_word_at_cursor(&search_range.start, &inline_layout) {
4103 Some(r) => r,
4104 None => return false,
4105 }
4106 } else {
4107 search_range
4108 };
4109
4110 let content = self.get_text_before_textinput(node_id.dom, dom_node_id);
4112 let full_text = self.extract_text_from_inline_content(&content);
4113
4114 let start_byte = word_range.start.cluster_id.start_byte_in_run as usize;
4116 let end_byte = word_range.end.cluster_id.start_byte_in_run as usize;
4117 let search_text = if word_range.start.cluster_id.source_run == word_range.end.cluster_id.source_run {
4118 if let Some(InlineContent::Text(run)) = content.get(word_range.start.cluster_id.source_run as usize) {
4119 if start_byte <= end_byte && end_byte <= run.text.len() {
4120 run.text[start_byte..end_byte].to_string()
4121 } else {
4122 return false;
4123 }
4124 } else {
4125 return false;
4126 }
4127 } else {
4128 return false; };
4130
4131 if search_text.is_empty() {
4132 return false;
4133 }
4134
4135 let mc = self.text_edit_manager.multi_cursor.as_ref().unwrap();
4137 let last_end_byte = mc.selections.last()
4138 .map_or(0, |s| match &s.selection {
4139 Selection::Range(r) => r.end.cluster_id.start_byte_in_run as usize,
4140 Selection::Cursor(c) => c.cluster_id.start_byte_in_run as usize,
4141 });
4142
4143 let search_run = word_range.start.cluster_id.source_run;
4144
4145 if let Some(InlineContent::Text(run)) = content.get(search_run as usize) {
4147 let search_in = &run.text;
4148 if let Some(offset) = search_in[last_end_byte..].find(&search_text) {
4150 let match_start = last_end_byte + offset;
4151 let match_end = match_start + search_text.len();
4152
4153 let new_range = SelectionRange {
4154 start: TextCursor {
4155 cluster_id: GraphemeClusterId {
4156 source_run: search_run,
4157 start_byte_in_run: match_start as u32,
4158 },
4159 affinity: CursorAffinity::Leading,
4160 },
4161 end: TextCursor {
4162 cluster_id: GraphemeClusterId {
4163 source_run: search_run,
4164 start_byte_in_run: match_end as u32,
4165 },
4166 affinity: CursorAffinity::Trailing,
4167 },
4168 };
4169
4170 let mc = self.text_edit_manager.multi_cursor.as_mut().unwrap();
4172 if need_word_expand {
4173 if let Some(first) = mc.selections.first_mut() {
4174 first.selection = Selection::Range(word_range);
4175 }
4176 }
4177 let _ = mc.add_selection(new_range);
4178 self.text_edit_manager.mark_dirty();
4179 return true;
4180 } else if last_end_byte > 0 {
4181 if let Some(offset) = search_in[..start_byte].find(&search_text) {
4183 let match_start = offset;
4184 let match_end = match_start + search_text.len();
4185
4186 let new_range = SelectionRange {
4187 start: TextCursor {
4188 cluster_id: GraphemeClusterId {
4189 source_run: search_run,
4190 start_byte_in_run: match_start as u32,
4191 },
4192 affinity: CursorAffinity::Leading,
4193 },
4194 end: TextCursor {
4195 cluster_id: GraphemeClusterId {
4196 source_run: search_run,
4197 start_byte_in_run: match_end as u32,
4198 },
4199 affinity: CursorAffinity::Trailing,
4200 },
4201 };
4202
4203 let mc = self.text_edit_manager.multi_cursor.as_mut().unwrap();
4204 if need_word_expand {
4205 if let Some(first) = mc.selections.first_mut() {
4206 first.selection = Selection::Range(word_range);
4207 }
4208 }
4209 let _ = mc.add_selection(new_range);
4210 self.text_edit_manager.mark_dirty();
4211 return true;
4212 }
4213 }
4214 }
4215
4216 if need_word_expand {
4219 let mc = self.text_edit_manager.multi_cursor.as_mut().unwrap();
4220 if let Some(first) = mc.selections.first_mut() {
4221 first.selection = Selection::Range(word_range);
4222 }
4223 self.text_edit_manager.mark_dirty();
4224 return true;
4225 }
4226
4227 false
4228 }
4229
4230 pub fn get_focused_cursor_rect_viewport(&self) -> Option<LogicalRect> {
4248 let mut cursor_rect = self.get_focused_cursor_rect()?;
4250
4251 let focused_node = self.focus_manager.focused_node?;
4253
4254 let layout_tree = self.layout_cache.tree.as_ref()?;
4256
4257 let target_node_id = focused_node.node.into_crate_internal();
4259 let layout_idx = layout_tree
4260 .nodes
4261 .iter()
4262 .position(|node| node.dom_node_id == target_node_id)?;
4263
4264 let gpu_cache = self.gpu_state_manager.caches.get(&focused_node.dom);
4266
4267 let mut current_layout_idx = layout_idx;
4271
4272 while let Some(parent_idx) = layout_tree.nodes.get(current_layout_idx)?.parent {
4273 if let Some(parent_dom_node_id) = layout_tree.nodes.get(parent_idx)?.dom_node_id {
4275 if let Some(scroll_state) = self
4277 .scroll_manager
4278 .get_scroll_state(focused_node.dom, parent_dom_node_id)
4279 {
4280 cursor_rect.origin.x -= scroll_state.current_offset.x;
4282 cursor_rect.origin.y -= scroll_state.current_offset.y;
4283 }
4284
4285 if let Some(cache) = gpu_cache {
4287 if let Some(transform) = cache.current_transform_values.get(&parent_dom_node_id)
4288 {
4289 let inverse = transform.inverse();
4292 if let Some(transformed_origin) =
4293 inverse.transform_point2d(cursor_rect.origin)
4294 {
4295 cursor_rect.origin = transformed_origin;
4296 }
4297 }
4299 }
4300 }
4301
4302 current_layout_idx = parent_idx;
4304 }
4305
4306 Some(cursor_rect)
4307 }
4308
4309 pub fn find_scrollable_ancestor(
4313 &self,
4314 mut node_id: DomNodeId,
4315 ) -> Option<DomNodeId> {
4316 let layout_tree = self.layout_cache.tree.as_ref()?;
4318
4319 let mut current_node_id = node_id.node.into_crate_internal();
4321
4322 loop {
4324 let layout_idx = layout_tree
4326 .nodes
4327 .iter()
4328 .position(|node| node.dom_node_id == current_node_id)?;
4329
4330 if layout_tree.warm(layout_idx).and_then(|w| w.scrollbar_info.as_ref()).is_some() {
4332 let check_node_id = current_node_id?;
4334 if self
4335 .scroll_manager
4336 .get_scroll_state(node_id.dom, check_node_id)
4337 .is_some()
4338 {
4339 return Some(DomNodeId {
4341 dom: node_id.dom,
4342 node: NodeHierarchyItemId::from_crate_internal(
4343 Some(check_node_id),
4344 ),
4345 });
4346 }
4347 }
4348
4349 let parent_idx = layout_tree.get(layout_idx)?.parent?;
4351 let parent_node = layout_tree.get(parent_idx)?;
4352 current_node_id = parent_node.dom_node_id;
4353 }
4354 }
4355
4356 pub fn scroll_selection_into_view(
4382 &mut self,
4383 scroll_type: SelectionScrollType,
4384 scroll_mode: ScrollMode,
4385 ) -> bool {
4386 let bounds = match scroll_type {
4388 SelectionScrollType::Cursor => {
4389 match self.get_focused_cursor_rect() {
4391 Some(rect) => rect,
4392 None => return false, }
4394 }
4395 SelectionScrollType::Selection => {
4396 match self.calculate_selection_bounding_rect()
4399 .or_else(|| self.get_focused_cursor_rect())
4400 {
4401 Some(rect) => rect,
4402 None => return false,
4403 }
4404 }
4405 SelectionScrollType::DragSelection { mouse_position } => {
4406 LogicalRect::new(mouse_position, LogicalSize::zero())
4408 }
4409 };
4410
4411 let Some(focused_node) = self.focus_manager.focused_node else {
4413 return false;
4414 };
4415
4416 let Some(scroll_container) = self.find_scrollable_ancestor(focused_node) else {
4418 return false; };
4420
4421 let Some(layout_tree) = self.layout_cache.tree.as_ref() else {
4423 return false;
4424 };
4425
4426 let Some(scrollable_node_internal) = scroll_container.node.into_crate_internal() else {
4427 return false;
4428 };
4429
4430 let Some(layout_idx) = layout_tree
4431 .nodes
4432 .iter()
4433 .position(|n| n.dom_node_id == Some(scrollable_node_internal))
4434 else {
4435 return false;
4436 };
4437
4438 let Some(scrollable_layout_node) = layout_tree.nodes.get(layout_idx) else {
4439 return false;
4440 };
4441
4442 let container_pos = self
4443 .layout_cache
4444 .calculated_positions
4445 .get(layout_idx)
4446 .copied()
4447 .unwrap_or_default();
4448
4449 let container_size = scrollable_layout_node.used_size.unwrap_or_default();
4450
4451 let container_rect = LogicalRect {
4452 origin: container_pos,
4453 size: container_size,
4454 };
4455
4456 let Some(scroll_state) = self
4458 .scroll_manager
4459 .get_scroll_state(scroll_container.dom, scrollable_node_internal)
4460 else {
4461 return false;
4462 };
4463
4464 let visible_area = LogicalRect::new(
4466 LogicalPosition::new(
4467 container_rect.origin.x + scroll_state.current_offset.x,
4468 container_rect.origin.y + scroll_state.current_offset.y,
4469 ),
4470 container_rect.size,
4471 );
4472
4473 let scroll_delta = match scroll_mode {
4475 ScrollMode::Instant => {
4476 calculate_instant_scroll_delta(bounds, visible_area)
4478 }
4479 ScrollMode::Accelerated => {
4480 let distance = calculate_edge_distance(bounds, visible_area);
4482 calculate_accelerated_scroll_delta(distance)
4483 }
4484 };
4485
4486 if scroll_delta.x != 0.0 || scroll_delta.y != 0.0 {
4488 let duration = match scroll_mode {
4489 ScrollMode::Instant => Duration::System(SystemTimeDiff { secs: 0, nanos: 0 }),
4490 ScrollMode::Accelerated => Duration::System(SystemTimeDiff {
4491 secs: 0,
4492 nanos: 16_666_667,
4493 }), };
4495
4496 let external = ExternalSystemCallbacks::rust_internal();
4497 let now = (external.get_system_time_fn.cb)();
4498
4499 let new_target = LogicalPosition {
4501 x: scroll_state.current_offset.x + scroll_delta.x,
4502 y: scroll_state.current_offset.y + scroll_delta.y,
4503 };
4504
4505 self.scroll_manager.scroll_to(
4506 scroll_container.dom,
4507 scrollable_node_internal,
4508 new_target,
4509 duration,
4510 EasingFunction::Linear,
4511 now,
4512 );
4513
4514 true } else {
4516 false }
4518 }
4519
4520 fn scroll_focused_cursor_into_view(&mut self) {
4525 self.scroll_selection_into_view(SelectionScrollType::Cursor, ScrollMode::Instant);
4527 }
4528}
4529
4530#[derive(Debug, Clone, Copy)]
4532pub enum SelectionScrollType {
4533 Cursor,
4535 Selection,
4537 DragSelection { mouse_position: LogicalPosition },
4539}
4540
4541#[derive(Debug, Clone, Copy)]
4543pub enum ScrollMode {
4544 Instant,
4546 Accelerated,
4548}
4549
4550#[derive(Debug, Clone, Copy)]
4552struct EdgeDistance {
4553 left: f32,
4554 right: f32,
4555 top: f32,
4556 bottom: f32,
4557}
4558
4559fn calculate_edge_distance(rect: LogicalRect, container: LogicalRect) -> EdgeDistance {
4561 EdgeDistance {
4562 left: (rect.origin.x - container.origin.x).max(0.0),
4564 right: ((container.origin.x + container.size.width) - (rect.origin.x + rect.size.width))
4566 .max(0.0),
4567 top: (rect.origin.y - container.origin.y).max(0.0),
4569 bottom: ((container.origin.y + container.size.height) - (rect.origin.y + rect.size.height))
4571 .max(0.0),
4572 }
4573}
4574
4575fn calculate_instant_scroll_delta(
4577 bounds: LogicalRect,
4578 visible_area: LogicalRect,
4579) -> LogicalPosition {
4580 const PADDING: f32 = 5.0;
4581 let mut delta = LogicalPosition::zero();
4582
4583 if bounds.origin.x < visible_area.origin.x + PADDING {
4585 delta.x = bounds.origin.x - visible_area.origin.x - PADDING;
4586 } else if bounds.origin.x + bounds.size.width
4587 > visible_area.origin.x + visible_area.size.width - PADDING
4588 {
4589 delta.x = (bounds.origin.x + bounds.size.width)
4590 - (visible_area.origin.x + visible_area.size.width)
4591 + PADDING;
4592 }
4593
4594 if bounds.origin.y < visible_area.origin.y + PADDING {
4596 delta.y = bounds.origin.y - visible_area.origin.y - PADDING;
4597 } else if bounds.origin.y + bounds.size.height
4598 > visible_area.origin.y + visible_area.size.height - PADDING
4599 {
4600 delta.y = (bounds.origin.y + bounds.size.height)
4601 - (visible_area.origin.y + visible_area.size.height)
4602 + PADDING;
4603 }
4604
4605 delta
4606}
4607
4608fn calculate_accelerated_scroll_delta(distance: EdgeDistance) -> LogicalPosition {
4610 const DEAD_ZONE: f32 = 20.0;
4612 const SLOW_ZONE: f32 = 50.0;
4613 const MEDIUM_ZONE: f32 = 100.0;
4614 const FAST_ZONE: f32 = 200.0;
4615
4616 const SLOW_SPEED: f32 = 2.0;
4618 const MEDIUM_SPEED: f32 = 4.0;
4619 const FAST_SPEED: f32 = 8.0;
4620 const VERY_FAST_SPEED: f32 = 16.0;
4621
4622 let speed_for_distance = |dist: f32| -> f32 {
4624 if dist < DEAD_ZONE {
4625 0.0
4626 } else if dist < SLOW_ZONE {
4627 SLOW_SPEED
4628 } else if dist < MEDIUM_ZONE {
4629 MEDIUM_SPEED
4630 } else if dist < FAST_ZONE {
4631 FAST_SPEED
4632 } else {
4633 VERY_FAST_SPEED
4634 }
4635 };
4636
4637 let scroll_x = if distance.left < distance.right {
4639 -speed_for_distance(distance.left)
4641 } else {
4642 speed_for_distance(distance.right)
4644 };
4645
4646 let scroll_y = if distance.top < distance.bottom {
4648 -speed_for_distance(distance.top)
4650 } else {
4651 speed_for_distance(distance.bottom)
4653 };
4654
4655 LogicalPosition::new(scroll_x, scroll_y)
4656}
4657
4658#[derive(Debug)]
4660pub struct LayoutResult {
4661 pub display_list: DisplayList,
4662 pub warnings: Vec<String>,
4663}
4664
4665impl LayoutResult {
4666 #[must_use] pub const fn new(display_list: DisplayList, warnings: Vec<String>) -> Self {
4667 Self {
4668 display_list,
4669 warnings,
4670 }
4671 }
4672}
4673
4674impl LayoutWindow {
4675 #[cfg(feature = "std")]
4680 #[allow(clippy::needless_pass_by_value)]
4685 pub fn run_single_timer(
4689 &mut self,
4690 timer_id: usize,
4691 frame_start: Instant,
4692 current_window_handle: &RawWindowHandle,
4693 gl_context: &OptionGlContextPtr,
4694 system_style: Arc<azul_css::system::SystemStyle>,
4695 system_callbacks: &ExternalSystemCallbacks,
4696 previous_window_state: &Option<FullWindowState>,
4697 current_window_state: &FullWindowState,
4698 renderer_resources: &RendererResources,
4699 ) -> (Vec<crate::callbacks::CallbackChange>, Update) {
4700 use crate::callbacks::{CallbackInfo, CallbackChange};
4701
4702 let mut update = Update::DoNothing;
4703 let mut all_changes = Vec::new();
4704 let mut should_terminate = TerminateTimer::Continue;
4705
4706 let current_scroll_states_nested = self.get_nested_scroll_states(DomId::ROOT_ID);
4707
4708 let timer_exists = self.timers.contains_key(&TimerId { id: timer_id });
4709 let timer_node_id = self
4710 .timers
4711 .get(&TimerId { id: timer_id })
4712 .and_then(|t| t.node_id.into_option());
4713
4714 if timer_exists {
4715 let hit_dom_node = timer_node_id.map_or_else(|| DomNodeId {
4716 dom: DomId::ROOT_ID,
4717 node: NodeHierarchyItemId::from_crate_internal(None),
4718 }, |s| s);
4719 let cursor_relative_to_item = OptionLogicalPosition::None;
4720 let cursor_in_viewport = OptionLogicalPosition::None;
4721
4722 let callback_changes = Arc::new(std::sync::Mutex::new(Vec::new()));
4723
4724 let timer_ctx = self
4725 .timers
4726 .get(&TimerId { id: timer_id })
4727 .map_or(OptionRefAny::None, |t| t.callback.ctx.clone());
4728
4729 let ref_data = crate::callbacks::CallbackInfoRefData {
4730 layout_window: self,
4731 renderer_resources,
4732 previous_window_state,
4733 current_window_state,
4734 gl_context,
4735 current_scroll_manager: ¤t_scroll_states_nested,
4736 current_window_handle,
4737 system_callbacks,
4738 system_style,
4739 monitors: self.monitors.clone(),
4740 #[cfg(feature = "icu")]
4741 icu_localizer: self.icu_localizer.clone(),
4742 ctx: timer_ctx,
4743 };
4744
4745 let callback_info = CallbackInfo::new(
4746 &ref_data,
4747 &callback_changes,
4748 hit_dom_node,
4749 cursor_relative_to_item,
4750 cursor_in_viewport,
4751 );
4752
4753 let timer = self.timers.get_mut(&TimerId { id: timer_id }).unwrap();
4754 let tcr = timer.invoke(&callback_info, &system_callbacks.get_system_time_fn);
4755
4756 update = tcr.should_update;
4757 should_terminate = tcr.should_terminate;
4758
4759 all_changes = callback_changes
4760 .lock()
4761 .map(|mut guard| core::mem::take(&mut *guard))
4762 .unwrap_or_default();
4763 }
4764
4765 if should_terminate == TerminateTimer::Terminate {
4766 all_changes.push(CallbackChange::RemoveTimer {
4767 timer_id: TimerId { id: timer_id },
4768 });
4769 }
4770
4771 (all_changes, update)
4772 }
4773
4774 #[cfg(feature = "std")]
4775 #[allow(clippy::needless_pass_by_value)]
4779 pub fn run_all_threads(
4780 &mut self,
4781 data: &mut RefAny,
4782 current_window_handle: &RawWindowHandle,
4783 gl_context: &OptionGlContextPtr,
4784 system_style: Arc<azul_css::system::SystemStyle>,
4785 system_callbacks: &ExternalSystemCallbacks,
4786 previous_window_state: &Option<FullWindowState>,
4787 current_window_state: &FullWindowState,
4788 renderer_resources: &RendererResources,
4789 ) -> (Vec<crate::callbacks::CallbackChange>, Update) {
4790 use std::collections::BTreeSet;
4791
4792 use crate::{
4793 callbacks::{CallbackInfo, CallbackChange},
4794 thread::{OptionThreadReceiveMsg, ThreadReceiveMsg, ThreadWriteBackMsg},
4795 };
4796
4797 let mut update = Update::DoNothing;
4798 let mut all_changes = Vec::new();
4799
4800 let current_scroll_states = self.get_nested_scroll_states(DomId::ROOT_ID);
4801
4802 let thread_ids: Vec<ThreadId> = self.threads.keys().copied().collect();
4803
4804 for thread_id in thread_ids {
4805 let Some(thread) = self.threads.get_mut(&thread_id) else {
4806 continue;
4807 };
4808
4809 let hit_dom_node = DomNodeId {
4810 dom: DomId::ROOT_ID,
4811 node: NodeHierarchyItemId::from_crate_internal(None),
4812 };
4813 let cursor_relative_to_item = OptionLogicalPosition::None;
4814 let cursor_in_viewport = OptionLogicalPosition::None;
4815
4816 let (msg, writeback_data_ptr, is_finished) = {
4817 let thread_inner = &mut *if let Ok(s) = thread.ptr.lock() { s } else {
4818 all_changes.push(CallbackChange::RemoveThread { thread_id });
4819 continue;
4820 };
4821
4822 let _ = thread_inner.sender_send(ThreadSendMsg::Tick);
4823 let recv = thread_inner.receiver_try_recv();
4824 let msg = match recv {
4825 OptionThreadReceiveMsg::None => continue,
4826 OptionThreadReceiveMsg::Some(s) => s,
4827 };
4828
4829 let writeback_data_ptr: *mut RefAny = &raw mut thread_inner.writeback_data;
4830 let is_finished = thread_inner.is_finished();
4831
4832 (msg, writeback_data_ptr, is_finished)
4833 };
4834
4835 let ThreadWriteBackMsg {
4836 refany: mut data_inner,
4837 callback,
4838 } = match msg {
4839 ThreadReceiveMsg::Update(update_screen) => {
4840 update.max_self(update_screen);
4841 continue;
4842 }
4843 ThreadReceiveMsg::WriteBack(t) => t,
4844 };
4845
4846 let callback_changes = Arc::new(std::sync::Mutex::new(Vec::new()));
4847
4848 let ref_data = crate::callbacks::CallbackInfoRefData {
4849 layout_window: self,
4850 renderer_resources,
4851 previous_window_state,
4852 current_window_state,
4853 gl_context,
4854 current_scroll_manager: ¤t_scroll_states,
4855 current_window_handle,
4856 system_callbacks,
4857 system_style: system_style.clone(),
4858 monitors: self.monitors.clone(),
4859 #[cfg(feature = "icu")]
4860 icu_localizer: self.icu_localizer.clone(),
4861 ctx: callback.ctx.clone(),
4862 };
4863
4864 let callback_info = CallbackInfo::new(
4865 &ref_data,
4866 &callback_changes,
4867 hit_dom_node,
4868 cursor_relative_to_item,
4869 cursor_in_viewport,
4870 );
4871
4872 let callback_update = (callback.cb)(
4873 unsafe { (*writeback_data_ptr).clone() },
4874 data_inner.clone(),
4875 callback_info,
4876 );
4877 update.max_self(callback_update);
4878
4879 let collected_changes = callback_changes
4880 .lock()
4881 .map(|mut guard| core::mem::take(&mut *guard))
4882 .unwrap_or_default();
4883
4884 all_changes.extend(collected_changes);
4885
4886 if is_finished {
4887 all_changes.push(CallbackChange::RemoveThread { thread_id });
4888 }
4889 }
4890
4891 (all_changes, update)
4892 }
4893
4894 pub fn invoke_single_callback(
4899 &mut self,
4900 callback: &mut Callback,
4901 data: &mut RefAny,
4902 current_window_handle: &RawWindowHandle,
4903 gl_context: &OptionGlContextPtr,
4904 system_style: Arc<azul_css::system::SystemStyle>,
4905 system_callbacks: &ExternalSystemCallbacks,
4906 previous_window_state: &Option<FullWindowState>,
4907 current_window_state: &FullWindowState,
4908 renderer_resources: &RendererResources,
4909 ) -> (Vec<crate::callbacks::CallbackChange>, Update) {
4910 let hit_dom_node = DomNodeId {
4913 dom: DomId::ROOT_ID,
4914 node: NodeHierarchyItemId::from_crate_internal(None),
4915 };
4916 self.invoke_single_callback_at(
4917 hit_dom_node,
4918 callback,
4919 data,
4920 current_window_handle,
4921 gl_context,
4922 system_style,
4923 system_callbacks,
4924 previous_window_state,
4925 current_window_state,
4926 renderer_resources,
4927 )
4928 }
4929
4930 pub fn invoke_single_callback_at(
4936 &mut self,
4937 hit_dom_node: DomNodeId,
4938 callback: &mut Callback,
4939 data: &mut RefAny,
4940 current_window_handle: &RawWindowHandle,
4941 gl_context: &OptionGlContextPtr,
4942 system_style: Arc<azul_css::system::SystemStyle>,
4943 system_callbacks: &ExternalSystemCallbacks,
4944 previous_window_state: &Option<FullWindowState>,
4945 current_window_state: &FullWindowState,
4946 renderer_resources: &RendererResources,
4947 ) -> (Vec<crate::callbacks::CallbackChange>, Update) {
4948 use crate::callbacks::{CallbackInfo, CallbackChange};
4949
4950 let current_scroll_states = self.get_nested_scroll_states(DomId::ROOT_ID);
4951
4952 let cursor_relative_to_item = match hit_dom_node.node.into_crate_internal() {
4960 Some(node_id) => self
4961 .hover_manager
4962 .get_current(&crate::managers::hover::InputPointId::Mouse)
4963 .and_then(|ht| ht.hovered_nodes.get(&hit_dom_node.dom))
4964 .and_then(|hit| hit.regular_hit_test_nodes.get(&node_id))
4965 .map_or(OptionLogicalPosition::None, |item| OptionLogicalPosition::Some(item.point_relative_to_item)),
4966 None => OptionLogicalPosition::None,
4967 };
4968 let cursor_in_viewport = current_window_state.mouse_state.cursor_position.get_position().map_or(OptionLogicalPosition::None, OptionLogicalPosition::Some);
4969
4970 let callback_changes = Arc::new(std::sync::Mutex::new(Vec::new()));
4972
4973 let ref_data = crate::callbacks::CallbackInfoRefData {
4981 layout_window: self,
4982 renderer_resources,
4983 previous_window_state,
4984 current_window_state,
4985 gl_context,
4986 current_scroll_manager: ¤t_scroll_states,
4987 current_window_handle,
4988 system_callbacks,
4989 system_style,
4990 monitors: self.monitors.clone(),
4991 #[cfg(feature = "icu")]
4992 icu_localizer: self.icu_localizer.clone(),
4993 ctx: callback.ctx.clone(),
4994 };
4995
4996 let callback_info = CallbackInfo::new(
4997 &ref_data,
4998 &callback_changes,
4999 hit_dom_node,
5000 cursor_relative_to_item,
5001 cursor_in_viewport,
5002 );
5003
5004 let update = (callback.cb)(data.clone(), callback_info);
5005
5006 let collected_changes = callback_changes
5008 .lock()
5009 .map(|mut guard| core::mem::take(&mut *guard))
5010 .unwrap_or_default();
5011
5012 (collected_changes, update)
5013 }
5014
5015 pub fn set_system_style(&mut self, system_style: Arc<azul_css::system::SystemStyle>) {
5023 #[cfg(feature = "icu")]
5024 {
5025 self.icu_localizer = crate::icu::IcuLocalizerHandle::from_system_language(&system_style.language);
5026 }
5027 self.system_style = Some(system_style);
5028 }
5029}
5030
5031#[cfg(feature = "icu")]
5034impl LayoutWindow {
5035 pub fn set_icu_locale(&mut self, locale: &str) {
5043 self.icu_localizer.set_locale(locale);
5044 }
5045
5046 pub fn init_icu_from_system_style(&mut self, system_style: &azul_css::system::SystemStyle) {
5050 self.icu_localizer = IcuLocalizerHandle::from_system_language(&system_style.language);
5051 }
5052
5053 pub fn get_icu_localizer(&self) -> IcuLocalizerHandle {
5057 self.icu_localizer.clone()
5058 }
5059
5060 pub fn load_icu_data_blob(&mut self, data: Vec<u8>) -> bool {
5065 self.icu_localizer.load_data_blob(&data)
5066 }
5067}
5068
5069#[cfg(test)]
5070mod tests {
5071 use super::*;
5072 use crate::{thread::Thread, timer::Timer};
5073
5074 #[test]
5075 fn test_timer_add_remove() {
5076 let fc_cache = FcFontCache::default();
5077 let mut window = LayoutWindow::new(fc_cache).unwrap();
5078
5079 let timer_id = TimerId { id: 1 };
5080 let timer = Timer::default();
5081
5082 window.add_timer(timer_id, timer);
5084 assert!(window.get_timer(&timer_id).is_some());
5085 assert_eq!(window.get_timer_ids().len(), 1);
5086
5087 let removed = window.remove_timer(&timer_id);
5089 assert!(removed.is_some());
5090 assert!(window.get_timer(&timer_id).is_none());
5091 assert_eq!(window.get_timer_ids().len(), 0);
5092 }
5093
5094 #[test]
5095 fn test_timer_get_mut() {
5096 let fc_cache = FcFontCache::default();
5097 let mut window = LayoutWindow::new(fc_cache).unwrap();
5098
5099 let timer_id = TimerId { id: 1 };
5100 let timer = Timer::default();
5101
5102 window.add_timer(timer_id, timer);
5103
5104 let timer_mut = window.get_timer_mut(&timer_id);
5106 assert!(timer_mut.is_some());
5107 }
5108
5109 #[test]
5110 fn test_multiple_timers() {
5111 let fc_cache = FcFontCache::default();
5112 let mut window = LayoutWindow::new(fc_cache).unwrap();
5113
5114 let timer1 = TimerId { id: 1 };
5115 let timer2 = TimerId { id: 2 };
5116 let timer3 = TimerId { id: 3 };
5117
5118 window.add_timer(timer1, Timer::default());
5119 window.add_timer(timer2, Timer::default());
5120 window.add_timer(timer3, Timer::default());
5121
5122 assert_eq!(window.get_timer_ids().len(), 3);
5123
5124 window.remove_timer(&timer2);
5125 assert_eq!(window.get_timer_ids().len(), 2);
5126 assert!(window.get_timer(&timer1).is_some());
5127 assert!(window.get_timer(&timer2).is_none());
5128 assert!(window.get_timer(&timer3).is_some());
5129 }
5130
5131 #[test]
5136 fn test_gpu_cache_management() {
5137 let fc_cache = FcFontCache::default();
5138 let mut window = LayoutWindow::new(fc_cache).unwrap();
5139
5140 let dom_id = DomId { inner: 0 };
5141
5142 assert!(window.get_gpu_cache(&dom_id).is_none());
5144
5145 let cache = window.get_or_create_gpu_cache(dom_id);
5147 assert!(cache.transform_keys.is_empty());
5148
5149 assert!(window.get_gpu_cache(&dom_id).is_some());
5151
5152 let cache_mut = window.get_gpu_cache_mut(&dom_id);
5154 assert!(cache_mut.is_some());
5155 }
5156
5157 #[test]
5158 fn test_gpu_cache_multiple_doms() {
5159 let fc_cache = FcFontCache::default();
5160 let mut window = LayoutWindow::new(fc_cache).unwrap();
5161
5162 let dom1 = DomId { inner: 0 };
5163 let dom2 = DomId { inner: 1 };
5164
5165 window.get_or_create_gpu_cache(dom1);
5166 window.get_or_create_gpu_cache(dom2);
5167
5168 assert!(window.get_gpu_cache(&dom1).is_some());
5169 assert!(window.get_gpu_cache(&dom2).is_some());
5170 }
5171
5172 #[test]
5173 fn test_compute_cursor_type_empty_hit_test() {
5174 use crate::hit_test::FullHitTest;
5175
5176 let fc_cache = FcFontCache::default();
5177 let window = LayoutWindow::new(fc_cache).unwrap();
5178
5179 let empty_hit = FullHitTest::empty(None);
5180 let cursor_test = window.compute_cursor_type_hit_test(&empty_hit);
5181
5182 assert_eq!(
5184 cursor_test.cursor_icon,
5185 azul_core::window::MouseCursorType::Default
5186 );
5187 assert!(cursor_test.cursor_node.is_none());
5188 }
5189
5190 #[test]
5191 fn test_layout_result_access() {
5192 let fc_cache = FcFontCache::default();
5193 let window = LayoutWindow::new(fc_cache).unwrap();
5194
5195 let dom_id = DomId { inner: 0 };
5196
5197 assert!(window.get_layout_result(&dom_id).is_none());
5199 assert_eq!(window.get_dom_ids().len(), 0);
5200 }
5201
5202 #[test]
5205 fn test_scroll_manager_initialization() {
5206 let fc_cache = FcFontCache::default();
5207 let window = LayoutWindow::new(fc_cache).unwrap();
5208
5209 let dom_id = DomId::ROOT_ID;
5210 let node_id = NodeId::new(0);
5211
5212 let scroll_offsets = window.scroll_manager.get_scroll_states_for_dom(dom_id);
5214 assert!(scroll_offsets.is_empty());
5215
5216 let offset = window.scroll_manager.get_current_offset(dom_id, node_id);
5218 assert_eq!(offset, None);
5219 }
5220
5221 #[test]
5222 fn test_scroll_manager_tick_updates_activity() {
5223 let fc_cache = FcFontCache::default();
5224 let mut window = LayoutWindow::new(fc_cache).unwrap();
5225
5226 let dom_id = DomId::ROOT_ID;
5227 let node_id = NodeId::new(0);
5228
5229 #[cfg(feature = "std")]
5231 let now = Instant::now();
5232 #[cfg(not(feature = "std"))]
5233 let now = Instant::Tick(azul_core::task::SystemTick { tick_counter: 0 });
5234
5235 let scroll_input = crate::managers::scroll_state::ScrollInput {
5236 dom_id,
5237 node_id,
5238 delta: LogicalPosition::new(10.0, 20.0),
5239 timestamp: now,
5240 source: crate::managers::scroll_state::ScrollInputSource::WheelDiscrete,
5241 };
5242
5243 let should_start_timer = window
5244 .scroll_manager
5245 .record_scroll_input(scroll_input);
5246
5247 assert!(should_start_timer);
5249 }
5250
5251 #[test]
5252 fn test_scroll_manager_programmatic_scroll() {
5253 let fc_cache = FcFontCache::default();
5254 let mut window = LayoutWindow::new(fc_cache).unwrap();
5255
5256 let dom_id = DomId::ROOT_ID;
5257 let node_id = NodeId::new(0);
5258
5259 #[cfg(feature = "std")]
5260 let now = Instant::now();
5261 #[cfg(not(feature = "std"))]
5262 let now = Instant::Tick(azul_core::task::SystemTick { tick_counter: 0 });
5263
5264 window.scroll_manager.scroll_to(
5266 dom_id,
5267 node_id,
5268 LogicalPosition::new(100.0, 200.0),
5269 Duration::System(SystemTimeDiff::from_millis(300)),
5270 EasingFunction::EaseOut,
5271 now.clone(),
5272 );
5273
5274 let tick_result = window.scroll_manager.tick(now);
5275
5276 assert!(tick_result.needs_repaint);
5278 }
5279
5280
5281
5282 #[test]
5283 fn test_gpu_cache_scrollbar_opacity_keys() {
5284 let fc_cache = FcFontCache::default();
5285 let mut window = LayoutWindow::new(fc_cache).unwrap();
5286
5287 let dom_id = DomId::ROOT_ID;
5288 let node_id = NodeId::new(0);
5289
5290 let gpu_cache = window.get_or_create_gpu_cache(dom_id);
5292
5293 assert!(gpu_cache.scrollbar_v_opacity_keys.is_empty());
5295 assert!(gpu_cache.scrollbar_h_opacity_keys.is_empty());
5296
5297 let opacity_key = OpacityKey::unique();
5299 gpu_cache
5300 .scrollbar_v_opacity_keys
5301 .insert((dom_id, node_id), opacity_key);
5302 gpu_cache
5303 .scrollbar_v_opacity_values
5304 .insert((dom_id, node_id), 1.0);
5305
5306 assert_eq!(gpu_cache.scrollbar_v_opacity_keys.len(), 1);
5308 assert_eq!(
5309 gpu_cache.scrollbar_v_opacity_values.get(&(dom_id, node_id)),
5310 Some(&1.0)
5311 );
5312 }
5313
5314
5315}
5316
5317impl LayoutWindow {
5319 pub fn find_next_text_node(
5332 &self,
5333 dom_id: &DomId,
5334 current_node: NodeId,
5335 ) -> Option<(DomId, NodeId)> {
5336 let layout_result = self.get_layout_result(dom_id)?;
5337 let styled_dom = &layout_result.styled_dom;
5338
5339 let start_idx = current_node.index() + 1;
5341 let node_hierarchy = &styled_dom.node_hierarchy;
5342
5343 for i in start_idx..node_hierarchy.len() {
5344 let node_id = NodeId::new(i);
5345
5346 if Self::node_has_text_content(styled_dom, node_id) {
5348 if Self::is_text_selectable(styled_dom, node_id) {
5350 return Some((*dom_id, node_id));
5351 }
5352 }
5353 }
5354
5355 None
5356 }
5357
5358 pub fn find_prev_text_node(
5371 &self,
5372 dom_id: &DomId,
5373 current_node: NodeId,
5374 ) -> Option<(DomId, NodeId)> {
5375 let layout_result = self.get_layout_result(dom_id)?;
5376 let styled_dom = &layout_result.styled_dom;
5377
5378 let current_idx = current_node.index();
5380
5381 for i in (0..current_idx).rev() {
5382 let node_id = NodeId::new(i);
5383
5384 if Self::node_has_text_content(styled_dom, node_id) {
5386 if Self::is_text_selectable(styled_dom, node_id) {
5388 return Some((*dom_id, node_id));
5389 }
5390 }
5391 }
5392
5393 None
5394 }
5395
5396 fn find_last_text_child(&self, dom_id: DomId, parent_node_id: NodeId) -> Option<NodeId> {
5402 let layout_result = self.layout_results.get(&dom_id)?;
5403 let styled_dom = &layout_result.styled_dom;
5404 let node_data_container = styled_dom.node_data.as_container();
5405 let hierarchy_container = styled_dom.node_hierarchy.as_container();
5406
5407 let parent_type = node_data_container[parent_node_id].get_node_type();
5409 if matches!(parent_type, NodeType::Text(_)) {
5410 return Some(parent_node_id);
5411 }
5412
5413 let parent_item = &hierarchy_container[parent_node_id];
5415 let mut last_text_child: Option<NodeId> = None;
5416 let mut current_child = parent_item.first_child_id(parent_node_id);
5417 while let Some(child_id) = current_child {
5418 let child_type = node_data_container[child_id].get_node_type();
5419 if matches!(child_type, NodeType::Text(_)) {
5420 last_text_child = Some(child_id);
5421 }
5422 current_child = hierarchy_container[child_id].next_sibling_id();
5423 }
5424
5425 last_text_child
5426 }
5427
5428 fn node_has_text_content(styled_dom: &StyledDom, node_id: NodeId) -> bool {
5430 let node_data_container = styled_dom.node_data.as_container();
5432 let node_type = node_data_container[node_id].get_node_type();
5433 if matches!(node_type, NodeType::Text(_)) {
5434 return true;
5435 }
5436
5437 let hierarchy_container = styled_dom.node_hierarchy.as_container();
5439 let node_item = &hierarchy_container[node_id];
5440
5441 let mut current_child = node_item.first_child_id(node_id);
5443 while let Some(child_id) = current_child {
5444 let child_type = node_data_container[child_id].get_node_type();
5445 if matches!(child_type, NodeType::Text(_)) {
5446 return true;
5447 }
5448
5449 current_child = hierarchy_container[child_id].next_sibling_id();
5451 }
5452
5453 false
5454 }
5455
5456 fn is_text_selectable(styled_dom: &StyledDom, node_id: NodeId) -> bool {
5458 let node_state = &styled_dom.styled_nodes.as_container()[node_id].styled_node_state;
5459 solver3::getters::is_text_selectable(styled_dom, node_id, node_state)
5460 }
5461
5462 #[cfg(feature = "a11y")]
5481 #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)] #[allow(clippy::too_many_lines)] #[allow(clippy::needless_pass_by_value)] pub fn process_accessibility_action(
5485 &mut self,
5486 dom_id: DomId,
5487 node_id: NodeId,
5488 action: AccessibilityAction,
5489 now: Instant,
5490 ) -> BTreeMap<DomNodeId, (Vec<EventFilter>, bool)> {
5491 use crate::managers::text_input::TextInputSource;
5492
5493 let mut affected_nodes = BTreeMap::new();
5494
5495 match action {
5496 AccessibilityAction::Focus => {
5498 let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(node_id));
5499 let dom_node_id = DomNodeId {
5500 dom: dom_id,
5501 node: hierarchy_id,
5502 };
5503 self.focus_manager.set_focused_node(Some(dom_node_id));
5504
5505 if let Some(layout_result) = self.layout_results.get(&dom_id) {
5507 if let Some(styled_node) = layout_result
5508 .styled_dom
5509 .node_data
5510 .as_ref()
5511 .get(node_id.index())
5512 {
5513 let is_contenteditable = styled_node.is_contenteditable()
5517 || styled_node.attributes().as_ref().iter().any(|attr| {
5518 matches!(attr, AttributeType::ContentEditable(_))
5519 });
5520
5521 if is_contenteditable {
5522 let inline_layout = self.get_inline_layout_for_node(dom_id, node_id).cloned();
5525 if let Some(ref layout) = inline_layout {
5526 let cursor = layout.items.iter().rev()
5527 .find_map(|item| if let ShapedItem::Cluster(c) = &item.item {
5528 Some(TextCursor {
5529 cluster_id: c.source_cluster_id,
5530 affinity: CursorAffinity::Trailing,
5531 })
5532 } else { None })
5533 .unwrap_or(TextCursor {
5534 cluster_id: GraphemeClusterId { source_run: 0, start_byte_in_run: 0 },
5535 affinity: CursorAffinity::Trailing,
5536 });
5537 self.text_edit_manager.initialize_editing(cursor, dom_id, node_id, 0);
5538
5539 self.scroll_cursor_into_view_if_needed(dom_id, node_id, now.clone());
5541 }
5542 } else {
5543 self.text_edit_manager.clear_editing();
5545 }
5546 }
5547 }
5548
5549 self.scroll_to_node_if_needed(dom_id, node_id, now);
5551 }
5552 AccessibilityAction::Blur => {
5553 self.focus_manager.clear_focus();
5554 self.text_edit_manager.clear_editing();
5555 }
5556 AccessibilityAction::SetSequentialFocusNavigationStartingPoint => {
5557 let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(node_id));
5558 let dom_node_id = DomNodeId {
5559 dom: dom_id,
5560 node: hierarchy_id,
5561 };
5562 self.focus_manager.set_focused_node(Some(dom_node_id));
5563 self.text_edit_manager.clear_editing();
5565 }
5566
5567 AccessibilityAction::ScrollIntoView => {
5569 self.scroll_to_node_if_needed(dom_id, node_id, now);
5570 }
5571 AccessibilityAction::ScrollLeft |
5572 AccessibilityAction::ScrollRight |
5573 AccessibilityAction::ScrollUp |
5574 AccessibilityAction::ScrollDown => {
5575 let dom_node_id = DomNodeId {
5577 dom: dom_id,
5578 node: NodeHierarchyItemId::from_crate_internal(Some(node_id)),
5579 };
5580 let (scroll_dom, scroll_nid) = self.find_scrollable_ancestor(dom_node_id)
5581 .and_then(|a| Some((a.dom, a.node.into_crate_internal()?)))
5582 .unwrap_or((dom_id, node_id));
5583
5584 let bounds = self.get_node_bounds(scroll_dom, scroll_nid);
5586 let vp_h = bounds.map_or(600.0, |b| b.size.height as f32);
5587 let vp_w = bounds.map_or(800.0, |b| b.size.width as f32);
5588
5589 let (dx, dy) = match action {
5590 AccessibilityAction::ScrollLeft => (-vp_w * 0.75, 0.0),
5591 AccessibilityAction::ScrollRight => ( vp_w * 0.75, 0.0),
5592 AccessibilityAction::ScrollUp => (0.0, -vp_h * 0.75),
5593 AccessibilityAction::ScrollDown => (0.0, vp_h * 0.75),
5594 _ => unreachable!(),
5595 };
5596
5597 self.scroll_manager.scroll_by(
5598 scroll_dom,
5599 scroll_nid,
5600 LogicalPosition { x: dx, y: dy },
5601 std::time::Duration::from_millis(250).into(),
5602 EasingFunction::EaseOut,
5603 now,
5604 );
5605 }
5606 AccessibilityAction::SetScrollOffset(pos) => {
5607 self.scroll_manager.scroll_to(
5608 dom_id,
5609 node_id,
5610 pos,
5611 std::time::Duration::from_millis(0).into(),
5612 EasingFunction::Linear,
5613 now,
5614 );
5615 }
5616 AccessibilityAction::ScrollToPoint(pos) => {
5617 self.scroll_manager.scroll_to(
5618 dom_id,
5619 node_id,
5620 pos,
5621 std::time::Duration::from_millis(300).into(),
5622 EasingFunction::EaseInOut,
5623 now,
5624 );
5625 }
5626
5627 AccessibilityAction::Default => {
5631 let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(node_id));
5633 let dom_node_id = DomNodeId {
5634 dom: dom_id,
5635 node: hierarchy_id,
5636 };
5637
5638 let event_filter = EventFilter::Hover(HoverEventFilter::MouseUp);
5640
5641 affected_nodes.insert(dom_node_id, (vec![event_filter], false));
5642 }
5643
5644 AccessibilityAction::Increment | AccessibilityAction::Decrement => {
5645 let is_increment = matches!(action, AccessibilityAction::Increment);
5655
5656 let current_value = self.layout_results.get(&dom_id).and_then(|layout_result| {
5658 layout_result
5659 .styled_dom
5660 .node_data
5661 .as_ref()
5662 .get(node_id.index())
5663 .and_then(|styled_node| {
5664 styled_node
5666 .attributes()
5667 .as_ref()
5668 .iter()
5669 .find_map(|attr| {
5670 if let AttributeType::Value(v) = attr {
5671 Some(v.as_str().to_string())
5672 } else {
5673 None
5674 }
5675 })
5676 .or_else(|| {
5677 if let NodeType::Text(text) = styled_node.get_node_type() {
5679 Some(text.as_str().to_string())
5680 } else {
5681 None
5682 }
5683 })
5684 })
5685 });
5686
5687 if let Some(value_str) = current_value {
5689 let parsed: Result<f64, _> = value_str.trim().parse();
5690
5691 let new_value_str = parsed.map_or_else(|_| if is_increment {
5692 "1".to_string()
5693 } else {
5694 "-1".to_string()
5695 }, |num| {
5696 let new_num = if is_increment { num + 1.0 } else { num - 1.0 };
5698 if num.fract() == 0.0 {
5700 format!("{}", new_num as i64)
5701 } else {
5702 format!("{new_num}")
5703 }
5704 });
5705
5706 let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(node_id));
5708 let dom_node_id = DomNodeId {
5709 dom: dom_id,
5710 node: hierarchy_id,
5711 };
5712
5713 let old_inline_content = self.get_text_before_textinput(dom_id, node_id);
5715 let old_text = self.extract_text_from_inline_content(&old_inline_content);
5716
5717 self.text_input_manager.record_input(
5719 dom_node_id,
5720 new_value_str,
5721 old_text,
5722 TextInputSource::Accessibility,
5723 );
5724
5725 affected_nodes.insert(
5727 dom_node_id,
5728 (vec![EventFilter::Focus(FocusEventFilter::TextInput)], false),
5729 );
5730 }
5731 }
5732
5733 AccessibilityAction::Collapse | AccessibilityAction::Expand => {
5734 let event_type = match action {
5736 AccessibilityAction::Collapse => On::Collapse,
5737 AccessibilityAction::Expand => On::Expand,
5738 _ => unreachable!(),
5739 };
5740
5741 if let Some(layout_result) = self.layout_results.get(&dom_id) {
5743 if let Some(styled_node) = layout_result
5744 .styled_dom
5745 .node_data
5746 .as_ref()
5747 .get(node_id.index())
5748 {
5749 let has_callback = styled_node
5751 .callbacks
5752 .as_ref()
5753 .iter()
5754 .any(|cb| cb.event == event_type.into());
5755
5756 let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(node_id));
5757 let dom_node_id = DomNodeId {
5758 dom: dom_id,
5759 node: hierarchy_id,
5760 };
5761
5762 if has_callback {
5763 affected_nodes.insert(dom_node_id, (vec![event_type.into()], false));
5765 } else {
5766 affected_nodes.insert(
5768 dom_node_id,
5769 (vec![EventFilter::Hover(HoverEventFilter::MouseUp)], false),
5770 );
5771 }
5772 }
5773 }
5774 }
5775
5776 AccessibilityAction::ShowContextMenu => {
5778 let Some(layout_result) = self.layout_results.get(&dom_id) else {
5780 return affected_nodes;
5781 };
5782
5783 let Some(styled_node) = layout_result
5785 .styled_dom
5786 .node_data
5787 .as_ref()
5788 .get(node_id.index())
5789 else {
5790 return affected_nodes;
5791 };
5792
5793 let has_context_menu = styled_node.get_context_menu().is_some();
5795
5796 if has_context_menu {
5797 let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(node_id));
5800 let dom_node_id = DomNodeId { dom: dom_id, node: hierarchy_id };
5801 affected_nodes.insert(
5802 dom_node_id,
5803 (vec![EventFilter::Hover(
5804 HoverEventFilter::RightMouseDown,
5805 )], false),
5806 );
5807 }
5808 }
5809
5810 AccessibilityAction::ReplaceSelectedText(ref text) => {
5812 let nodes = self.edit_text_node(
5813 dom_id,
5814 node_id,
5815 &TextEditType::ReplaceSelection(text.as_str().to_string()),
5816 );
5817 for node in nodes {
5818 affected_nodes.insert(node, (Vec::new(), true)); }
5820 }
5821 AccessibilityAction::SetValue(ref text) => {
5822 let nodes = self.edit_text_node(
5823 dom_id,
5824 node_id,
5825 &TextEditType::SetValue(text.as_str().to_string()),
5826 );
5827 for node in nodes {
5828 affected_nodes.insert(node, (Vec::new(), true));
5829 }
5830 }
5831 AccessibilityAction::SetNumericValue(value) => {
5832 let nodes = self.edit_text_node(
5833 dom_id,
5834 node_id,
5835 &TextEditType::SetNumericValue(f64::from(value.get())),
5836 );
5837 for node in nodes {
5838 affected_nodes.insert(node, (Vec::new(), true));
5839 }
5840 }
5841 AccessibilityAction::SetTextSelection(selection) => {
5842 let text_layout = self.get_node_inline_layout(dom_id, node_id);
5844
5845 if let Some(inline_layout) = text_layout {
5846 let start_cursor = Self::byte_offset_to_cursor(
5848 inline_layout.as_ref(),
5849 selection.selection_start as u32,
5850 );
5851 let end_cursor = Self::byte_offset_to_cursor(
5852 inline_layout.as_ref(),
5853 selection.selection_end as u32,
5854 );
5855
5856 {
5857 let (start, end) = (start_cursor, end_cursor);
5858 let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(node_id));
5859 let dom_node_id = DomNodeId {
5860 dom: dom_id,
5861 node: hierarchy_id,
5862 };
5863
5864 let _ = end;
5867 if let Some(ref mut mc) = self.text_edit_manager.multi_cursor {
5868 mc.set_single_cursor(start);
5869 }
5870 }
5871 } else {
5872 }
5874 }
5875
5876 AccessibilityAction::ShowTooltip | AccessibilityAction::HideTooltip => {
5878 }
5880
5881 AccessibilityAction::CustomAction(_id) => {
5882 }
5884 }
5885
5886 affected_nodes
5887 }
5888
5889 pub fn record_text_input(
5911 &mut self,
5912 text_input: &str,
5913 ) -> BTreeMap<DomNodeId, (Vec<EventFilter>, bool)> {
5914 use std::collections::BTreeMap;
5915
5916 use crate::managers::text_input::TextInputSource;
5917
5918 let mut affected_nodes = BTreeMap::new();
5919
5920 if text_input.is_empty() {
5921 return affected_nodes;
5922 }
5923
5924 let Some(focused_node) = self.focus_manager.get_focused_node().copied() else {
5926 return affected_nodes;
5927 };
5928
5929 let Some(node_id) = focused_node.node.into_crate_internal() else {
5930 return affected_nodes;
5931 };
5932
5933 let old_inline_content = self.get_text_before_textinput(focused_node.dom, node_id);
5935 let old_text = self.extract_text_from_inline_content(&old_inline_content);
5936
5937 self.text_input_manager.record_input(
5939 focused_node,
5940 text_input.to_string(),
5941 old_text,
5942 TextInputSource::Keyboard, );
5944
5945 let text_input_event = vec![EventFilter::Focus(FocusEventFilter::TextInput)];
5947
5948 affected_nodes.insert(focused_node, (text_input_event, false)); affected_nodes
5951 }
5952
5953 #[allow(clippy::too_many_lines)] pub fn apply_text_changeset(&mut self) -> TextChangesetResult {
5964 use crate::managers::changeset::{TextChangeset, TextOpInsertText, TextOperation};
5965 use crate::text3::edit::{edit_text, TextEdit};
5966 static CHANGESET_COUNTER: AtomicUsize = AtomicUsize::new(0);
5967
5968 let empty = TextChangesetResult { dirty_nodes: Vec::new(), needs_relayout: false };
5970
5971 let changeset = match self.text_input_manager.get_pending_changeset() {
5972 Some(cs) => {
5973 cs.clone()
5974 }
5975 None => {
5976 return empty;
5977 }
5978 };
5979
5980 let Some(node_id) = changeset.node.node.into_crate_internal() else {
5981 self.text_input_manager.clear_changeset();
5982 return empty;
5983 };
5984
5985 let dom_id = changeset.node.dom;
5986
5987 let Some(layout_result) = self.layout_results.get(&dom_id) else {
5989 self.text_input_manager.clear_changeset();
5990 return empty;
5991 };
5992
5993 let Some(styled_node) = layout_result
5994 .styled_dom
5995 .node_data
5996 .as_ref()
5997 .get(node_id.index()) else {
5998 self.text_input_manager.clear_changeset();
5999 return empty;
6000 };
6001
6002 let is_contenteditable = styled_node.is_contenteditable()
6006 || styled_node.attributes().as_ref().iter().any(|attr| {
6007 matches!(attr, AttributeType::ContentEditable(_))
6008 });
6009
6010 if !is_contenteditable {
6011 self.text_input_manager.clear_changeset();
6012 return empty;
6013 }
6014
6015 let content = self.get_text_before_textinput(dom_id, node_id);
6017
6018 let mc_selections = self.text_edit_manager.multi_cursor.as_ref()
6020 .map(azul_core::selection::MultiCursorState::to_selections)
6021 .unwrap_or_default();
6022 let current_selection = if !mc_selections.is_empty() {
6023 mc_selections
6024 } else if let Some(cursor) = self.text_edit_manager.get_primary_cursor() {
6025 vec![Selection::Cursor(cursor)]
6026 } else {
6027 vec![Selection::Cursor(TextCursor {
6028 cluster_id: GraphemeClusterId {
6029 source_run: 0,
6030 start_byte_in_run: 0,
6031 },
6032 affinity: CursorAffinity::Leading,
6033 })]
6034 };
6035
6036 let old_text = self.extract_text_from_inline_content(&content);
6038 let old_cursor = current_selection.first().and_then(|sel| {
6039 if let Selection::Cursor(c) = sel {
6040 Some(*c)
6041 } else {
6042 None
6043 }
6044 });
6045 let old_selection_range = current_selection.first().and_then(|sel| {
6046 if let Selection::Range(r) = sel {
6047 Some(*r)
6048 } else {
6049 None
6050 }
6051 });
6052
6053 let pre_state = crate::managers::undo_redo::NodeStateSnapshot {
6054 node_id: NodeId::new(node_id.index()),
6055 text_content: old_text.into(),
6056 cursor_position: old_cursor.into(),
6057 selection_range: old_selection_range.into(),
6058 #[cfg(feature = "std")]
6059 timestamp: Instant::now(),
6060 #[cfg(not(feature = "std"))]
6061 timestamp: azul_core::task::Instant::Tick(azul_core::task::SystemTick { tick_counter: 0 }),
6062 };
6063
6064 let text_edit = TextEdit::Insert(changeset.inserted_text.as_str().to_string());
6066 let (new_content, new_selections) = edit_text(&content, ¤t_selection, &text_edit);
6067
6068 if let Some(ref mut mc) = self.text_edit_manager.multi_cursor {
6070 mc.update_from_edit_result(&new_selections);
6071 }
6072 let pre_content_snapshot = content;
6078 let post_content_snapshot = new_content.clone();
6079
6080 self.update_text_cache_after_edit(dom_id, node_id, new_content);
6082
6083 let new_cursor = self
6087 .get_focused_cursor_rect()
6088 .map_or(CursorPosition::Uninitialized, |r| CursorPosition::InWindow(r.origin));
6089
6090 let old_cursor_pos = old_cursor
6091 .as_ref()
6092 .map_or(CursorPosition::Uninitialized, |_| {
6093 self.get_focused_cursor_rect()
6098 .map_or(CursorPosition::Uninitialized, |r| CursorPosition::InWindow(r.origin))
6099 });
6100
6101 let changeset_id = CHANGESET_COUNTER.fetch_add(1, Ordering::SeqCst);
6103
6104 let undo_changeset = TextChangeset {
6105 id: changeset_id,
6106 target: changeset.node,
6107 operation: TextOperation::InsertText(TextOpInsertText {
6108 text: changeset.inserted_text,
6109 position: old_cursor_pos,
6110 new_cursor,
6111 }),
6112 #[cfg(feature = "std")]
6113 timestamp: Instant::now(),
6114 #[cfg(not(feature = "std"))]
6115 timestamp: azul_core::task::Instant::Tick(azul_core::task::SystemTick { tick_counter: 0 }),
6116 };
6117 self.undo_redo_manager
6118 .store_content_snapshot(changeset_id, pre_content_snapshot, post_content_snapshot);
6119 self.undo_redo_manager
6120 .record_operation(undo_changeset, pre_state);
6121
6122 self.text_input_manager.clear_changeset();
6124
6125 let now = Instant::now();
6130 self.text_edit_manager.blink.reset_blink_on_input(now);
6131
6132 let needs_relayout = self.dirty_text_nodes.values()
6134 .any(|d| d.needs_ancestor_relayout);
6135
6136 let dirty_nodes = self.determine_dirty_text_nodes(dom_id, node_id);
6138 TextChangesetResult { dirty_nodes, needs_relayout }
6139 }
6140
6141 fn determine_dirty_text_nodes(
6145 &self,
6146 dom_id: DomId,
6147 node_id: NodeId,
6148 ) -> Vec<DomNodeId> {
6149 let Some(layout_result) = self.layout_results.get(&dom_id) else {
6150 return Vec::new();
6151 };
6152
6153 let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(node_id));
6154 let node_dom_id = DomNodeId {
6155 dom: dom_id,
6156 node: hierarchy_id,
6157 };
6158
6159 let parent_id = layout_result
6161 .styled_dom
6162 .node_hierarchy
6163 .as_container()
6164 .get(node_id)
6165 .and_then(azul_core::styled_dom::NodeHierarchyItem::parent_id)
6166 .map(|parent_node_id| {
6167 let parent_hierarchy_id =
6168 NodeHierarchyItemId::from_crate_internal(Some(parent_node_id));
6169 DomNodeId {
6170 dom: dom_id,
6171 node: parent_hierarchy_id,
6172 }
6173 });
6174
6175 parent_id.map_or_else(|| vec![node_dom_id], |parent| vec![node_dom_id, parent])
6177 }
6178
6179 #[inline]
6181 pub fn process_text_input(
6182 &mut self,
6183 text_input: &str,
6184 ) -> BTreeMap<DomNodeId, (Vec<EventFilter>, bool)> {
6185 self.record_text_input(text_input)
6186 }
6187
6188 pub const fn get_last_text_changeset(&self) -> Option<&PendingTextEdit> {
6190 self.text_input_manager.get_pending_changeset()
6191 }
6192
6193 pub fn get_text_before_textinput(&self, dom_id: DomId, node_id: NodeId) -> Vec<InlineContent> {
6203 if let Some(dirty_node) = self.dirty_text_nodes.get(&(dom_id, node_id)) {
6209 return dirty_node.content.clone();
6210 }
6211
6212 let Some(layout_result) = self.layout_results.get(&dom_id) else {
6215 return Vec::new();
6216 };
6217
6218 let Some(node_data) = layout_result
6220 .styled_dom
6221 .node_data
6222 .as_ref()
6223 .get(node_id.index())
6224 else {
6225 return Vec::new();
6226 };
6227
6228 match node_data.get_node_type() {
6230 NodeType::Text(text) => {
6231 let style = self.get_text_style_for_node(dom_id, node_id);
6233
6234 vec![InlineContent::Text(StyledRun {
6235 text: text.as_str().to_string(),
6236 style,
6237 logical_start_byte: 0,
6238 source_node_id: Some(node_id),
6239 })]
6240 }
6241 NodeType::Div | NodeType::Body | NodeType::VirtualView => {
6242 self.collect_text_from_children(dom_id, node_id)
6244 }
6245 _ => {
6246 Vec::new()
6248 }
6249 }
6250 }
6251
6252 fn get_text_style_for_node(
6254 &self,
6255 dom_id: DomId,
6256 node_id: NodeId,
6257 ) -> Arc<StyleProperties> {
6258 use alloc::sync::Arc;
6259
6260 let Some(layout_result) = self.layout_results.get(&dom_id) else {
6261 return Arc::new(StyleProperties::default());
6262 };
6263
6264 let vp = layout_result.viewport.size;
6266 let props = solver3::getters::get_style_properties(
6267 &layout_result.styled_dom,
6268 node_id,
6269 self.system_style.as_ref(),
6270 azul_css::props::basic::PhysicalSize::new(vp.width, vp.height),
6271 );
6272
6273 Arc::new(props)
6274 }
6275
6276 fn collect_text_from_children(
6278 &self,
6279 dom_id: DomId,
6280 parent_node_id: NodeId,
6281 ) -> Vec<InlineContent> {
6282 let Some(layout_result) = self.layout_results.get(&dom_id) else {
6283 return Vec::new();
6284 };
6285
6286 let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_ref();
6287 let Some(parent_item) = node_hierarchy.get(parent_node_id.index()) else {
6288 return Vec::new();
6289 };
6290
6291 let mut result = Vec::new();
6292
6293 let mut current_child = parent_item.first_child_id(parent_node_id);
6295 while let Some(child_id) = current_child {
6296 let child_content = self.get_text_before_textinput(dom_id, child_id);
6298 result.extend(child_content);
6299
6300 let Some(child_item) = node_hierarchy.get(child_id.index()) else {
6302 break;
6303 };
6304 current_child = child_item.next_sibling_id();
6305 }
6306
6307 result
6308 }
6309
6310 #[allow(clippy::only_used_in_recursion)]
6317 pub fn extract_text_from_inline_content(&self, content: &[InlineContent]) -> String {
6318 let mut result = String::new();
6319
6320 for item in content {
6321 match item {
6322 InlineContent::Text(text_run) => {
6323 result.push_str(&text_run.text);
6324 }
6325 InlineContent::Space(_) => {
6326 result.push(' ');
6327 }
6328 InlineContent::LineBreak(_) => {
6329 result.push('\n');
6330 }
6331 InlineContent::Tab { .. } => {
6332 result.push('\t');
6333 }
6334 InlineContent::Ruby { base, .. } => {
6335 result.push_str(&self.extract_text_from_inline_content(base));
6337 }
6338 InlineContent::Marker { run, .. } => {
6339 result.push_str(&run.text);
6341 }
6342 InlineContent::Image(_) | InlineContent::Shape(_) => {}
6344 }
6345 }
6346
6347 result
6348 }
6349
6350 #[allow(clippy::needless_pass_by_value)]
6363 #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] pub fn update_text_cache_after_edit(
6365 &mut self,
6366 dom_id: DomId,
6367 node_id: NodeId,
6368 new_inline_content: Vec<InlineContent>,
6369 ) {
6370 use crate::solver3::layout_tree::CachedInlineLayout;
6371
6372 let cursor = self.text_edit_manager.get_primary_cursor();
6374 self.dirty_text_nodes.insert(
6375 (dom_id, node_id),
6376 DirtyTextNode {
6377 content: new_inline_content.clone(),
6378 cursor,
6379 needs_ancestor_relayout: false, },
6381 );
6382
6383 let (mut constraints, ifc_layout_index) = {
6389 let Some(layout_result) = self.layout_results.get(&dom_id) else {
6390 return;
6391 };
6392
6393 let mut found: Option<(usize, &CachedInlineLayout)> = None;
6395
6396 if let Some(layout_indices) = layout_result.layout_tree.dom_to_layout.get(&node_id) {
6398 for &idx in layout_indices {
6399 if let Some(w) = layout_result.layout_tree.warm(idx) {
6400 if let Some(ref cached) = w.inline_layout_result {
6401 found = Some((idx, cached));
6402 break;
6403 }
6404 }
6405 }
6406 }
6407
6408 if found.is_none() {
6410 let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_ref();
6411 if let Some(parent_item) = node_hierarchy.get(node_id.index()) {
6412 let mut child = parent_item.first_child_id(node_id);
6413 while let Some(child_id) = child {
6414 if let Some(child_indices) = layout_result.layout_tree.dom_to_layout.get(&child_id) {
6415 for &idx in child_indices {
6416 if let Some(w) = layout_result.layout_tree.warm(idx) {
6417 if let Some(ref cached) = w.inline_layout_result {
6418 found = Some((idx, cached));
6419 break;
6420 }
6421 }
6422 }
6423 }
6424 if found.is_some() { break; }
6425 child = node_hierarchy.get(child_id.index()).and_then(azul_core::styled_dom::NodeHierarchyItem::next_sibling_id);
6426 }
6427 }
6428 }
6429
6430 let Some((ifc_idx, cached_layout)) = found else {
6431 return;
6432 };
6433
6434 match &cached_layout.constraints {
6435 Some(c) => (c.clone(), ifc_idx),
6436 None => {
6437 return;
6438 }
6439 }
6440 };
6441
6442 if let Some(layout_result) = self.layout_results.get(&dom_id) {
6451 let mut found_width = false;
6452
6453 if let Some(layout_indices) = layout_result.layout_tree.dom_to_layout.get(&node_id) {
6455 for &idx in layout_indices {
6456 if let Some(container_node) = layout_result.layout_tree.get(idx) {
6457 if let Some(container_size) = container_node.used_size {
6458 let bp = container_node.box_props.unpack();
6459 let content_width = container_size.width
6460 - bp.padding.left - bp.padding.right
6461 - bp.border.left - bp.border.right;
6462 if content_width > 0.0 {
6463 constraints.available_width =
6464 crate::text3::cache::AvailableSpace::Definite(content_width);
6465 found_width = true;
6466 }
6467 break;
6468 }
6469 }
6470 }
6471 }
6472
6473 if !found_width {
6475 if let Some(parent_idx) = layout_result.layout_tree.get(ifc_layout_index)
6476 .and_then(|n| n.parent)
6477 {
6478 if let Some(parent_node) = layout_result.layout_tree.get(parent_idx) {
6479 if let Some(parent_size) = parent_node.used_size {
6480 let bp = parent_node.box_props.unpack();
6481 let content_width = parent_size.width
6482 - bp.padding.left - bp.padding.right
6483 - bp.border.left - bp.border.right;
6484 if content_width > 0.0 {
6485 constraints.available_width =
6486 crate::text3::cache::AvailableSpace::Definite(content_width);
6487 }
6488 }
6489 }
6490 }
6491 }
6492 }
6493
6494 let cached_snapshot = self
6503 .layout_results
6504 .get(&dom_id)
6505 .and_then(|lr| lr.layout_tree.warm(ifc_layout_index))
6506 .and_then(|w| w.inline_layout_result.as_ref())
6507 .cloned();
6508
6509 let new_layout = cached_snapshot.map_or_else(|| self.relayout_text_node_internal(&new_inline_content, &constraints), |cached| self.try_incremental_text_relayout(
6510 &new_inline_content,
6511 &constraints,
6512 &cached,
6513 node_id,
6514 )
6515 .map(|(layout, _skipped_fragment)| layout));
6516
6517 let Some(new_layout) = new_layout else {
6518 return;
6519 };
6520
6521 if let Some(layout_result) = self.layout_results.get_mut(&dom_id) {
6524 let old_size = layout_result.layout_tree.get(ifc_layout_index).and_then(|n| n.used_size);
6525 let new_bounds = new_layout.bounds();
6526 let new_size = Some(LogicalSize {
6527 width: new_bounds.width,
6528 height: new_bounds.height,
6529 });
6530
6531 if let (Some(old), Some(new)) = (old_size, new_size) {
6533 if (old.height - new.height).abs() > 0.5 || (old.width - new.width).abs() > 0.5 {
6534 if let Some(dirty_node) = self.dirty_text_nodes.get_mut(&(dom_id, node_id)) {
6536 dirty_node.needs_ancestor_relayout = true;
6537 }
6538 }
6539 }
6540
6541 if let Some(warm_node) = layout_result.layout_tree.warm_mut(ifc_layout_index) {
6543 warm_node.inline_layout_result = Some(CachedInlineLayout::new_with_constraints(
6544 Arc::new(new_layout),
6545 constraints.available_width,
6546 false, constraints,
6548 ));
6549 }
6550 }
6551
6552 self.regenerate_display_list_for_dom(dom_id);
6556 }
6557
6558 pub fn apply_preedit_to_text_cache(&mut self, dom_id: DomId, node_id: NodeId) {
6573 let preedit = match &self.text_edit_manager.preedit_text {
6574 Some(p) if !p.is_empty() => p.clone(),
6575 _ => {
6576 self.pre_preedit_content = None;
6578 self.reapply_dirty_text_node(dom_id, node_id);
6579 return;
6580 }
6581 };
6582
6583 let Some(cursor) = self.text_edit_manager.get_primary_cursor() else {
6584 return;
6585 };
6586
6587 if self.pre_preedit_content.is_none() {
6590 let original = self.get_text_before_textinput(dom_id, node_id);
6591 self.pre_preedit_content = Some(original);
6592 }
6593
6594 let mut content = self.pre_preedit_content.clone().unwrap();
6596
6597 let run_idx = cursor.cluster_id.source_run as usize;
6599 let byte_pos = cursor.cluster_id.start_byte_in_run as usize;
6600 if let Some(InlineContent::Text(run)) = content.get_mut(run_idx) {
6601 let clamped_pos = byte_pos.min(run.text.len());
6602 run.text.insert_str(clamped_pos, &preedit);
6603 }
6604
6605 self.update_text_cache_after_edit(dom_id, node_id, content);
6607 self.regenerate_display_list_for_dom(dom_id);
6608 }
6609
6610 pub fn reapply_dirty_text_node(&mut self, dom_id: DomId, node_id: NodeId) {
6611 let content = match self.dirty_text_nodes.get(&(dom_id, node_id)) {
6612 Some(dirty) => dirty.content.clone(),
6613 None => return,
6614 };
6615 self.update_text_cache_after_edit(dom_id, node_id, content);
6617 self.regenerate_display_list_for_dom(dom_id);
6619 }
6620
6621 pub fn regenerate_display_list_for_dom(&mut self, dom_id: DomId) {
6631 use crate::solver3::{
6632 display_list::generate_display_list,
6633 LayoutContext,
6634 };
6635
6636 let Some(layout_result) = self.layout_results.get(&dom_id) else {
6638 return;
6639 };
6640
6641 let tree = &layout_result.layout_tree;
6642 let calculated_positions = &layout_result.calculated_positions;
6643 let scroll_ids = &layout_result.scroll_ids;
6644 let styled_dom = &layout_result.styled_dom;
6645 let viewport = layout_result.viewport;
6646
6647 let scroll_offsets = self.scroll_manager.get_scroll_states_for_dom(dom_id);
6649
6650 let gpu_cache = self.gpu_state_manager.get_or_create_cache(dom_id).clone();
6652
6653 let cursor_is_visible = self.text_edit_manager.should_draw_cursor();
6655 let cursor_locations = self.text_edit_manager.build_cursor_locations();
6656 let text_selections_map = self.text_edit_manager.build_text_selections_map();
6657
6658 let mut counter_values = HashMap::new();
6660 let mut debug_messages: Option<Vec<LayoutDebugMessage>> = None;
6661 let cache_map = std::mem::take(&mut self.layout_cache.cache_map);
6662
6663 let mut ctx = LayoutContext {
6664 scrollbar_style_cache: core::cell::RefCell::new(HashMap::new()),
6665 styled_dom,
6666 font_manager: &self.font_manager,
6667 text_selections: &text_selections_map,
6668 debug_messages: &mut debug_messages,
6669 counters: &mut counter_values,
6670 viewport_size: viewport.size,
6671 fragmentation_context: None,
6672 cursor_is_visible,
6673 cursor_locations,
6674 preedit_text: self.text_edit_manager.preedit_text.clone(),
6675 cache_map,
6676 image_cache: &self.image_cache,
6677 system_style: self.system_style.clone(),
6678 get_system_time_fn: azul_core::task::GetSystemTimeCallback {
6679 cb: azul_core::task::get_system_time_libstd,
6680 },
6681 dirty_text_overrides: BTreeMap::new(),
6682 };
6683
6684 let new_display_list = generate_display_list(
6686 &mut ctx,
6687 tree,
6688 calculated_positions,
6689 &scroll_offsets,
6690 scroll_ids,
6691 Some(&gpu_cache),
6692 &self.renderer_resources,
6693 self.id_namespace,
6694 dom_id,
6695 );
6696
6697 self.layout_cache.cache_map = std::mem::take(&mut ctx.cache_map);
6699
6700 match new_display_list {
6701 Ok(display_list) => {
6702 if let Some(layout_result) = self.layout_results.get_mut(&dom_id) {
6703 layout_result.display_list = display_list;
6704 }
6705 self.text_edit_manager.display_list_dirty = false;
6712 #[cfg(feature = "a11y")]
6715 self.update_a11y_tree_incremental();
6716 }
6717 Err(_e) => {
6718 }
6719 }
6720 }
6721
6722 fn relayout_text_node_internal(
6724 &self,
6725 content: &[InlineContent],
6726 constraints: &UnifiedConstraints,
6727 ) -> Option<UnifiedLayout> {
6728 let (logical_items, shaped_items) = self.shape_text_for_relayout(content, constraints)?;
6729
6730 if logical_items.is_empty() {
6731 return Some(UnifiedLayout {
6732 items: Vec::new(),
6733 overflow: crate::text3::cache::OverflowInfo::default(),
6734 });
6735 }
6736
6737 self.fragment_layout_from_shaped(&logical_items, &shaped_items, constraints)
6738 }
6739
6740 fn shape_text_for_relayout(
6744 &self,
6745 content: &[InlineContent],
6746 constraints: &UnifiedConstraints,
6747 ) -> Option<(
6748 Vec<crate::text3::cache::LogicalItem>,
6749 Vec<ShapedItem>,
6750 )> {
6751 use crate::text3::cache::{
6752 create_logical_items, reorder_logical_items, shape_visual_items, BidiDirection,
6753 };
6754
6755 let logical_items = create_logical_items(content, &[], &mut None);
6756 if logical_items.is_empty() {
6757 return Some((logical_items, Vec::new()));
6758 }
6759
6760 let base_direction = constraints.direction.unwrap_or(BidiDirection::Ltr);
6761 let visual_items = reorder_logical_items(
6762 &logical_items,
6763 base_direction,
6764 crate::text3::cache::UnicodeBidi::Normal,
6765 &mut None,
6766 )
6767 .ok()?;
6768
6769 let loaded_fonts = self.font_manager.get_loaded_fonts();
6770 let shaped_items = shape_visual_items(
6771 &visual_items,
6772 self.font_manager.get_font_chain_cache(),
6773 &self.font_manager.fc_cache,
6774 &loaded_fonts,
6775 &mut None,
6776 )
6777 .ok()?;
6778
6779 Some((logical_items, shaped_items))
6780 }
6781
6782 fn fragment_layout_from_shaped(
6784 &self,
6785 logical_items: &[crate::text3::cache::LogicalItem],
6786 shaped_items: &[ShapedItem],
6787 constraints: &UnifiedConstraints,
6788 ) -> Option<UnifiedLayout> {
6789 use crate::text3::cache::{perform_fragment_layout, BreakCursor};
6790
6791 let loaded_fonts = self.font_manager.get_loaded_fonts();
6792 let mut cursor = BreakCursor::new(shaped_items);
6793 perform_fragment_layout(&mut cursor, logical_items, constraints, &mut None, &loaded_fonts).ok()
6794 }
6795
6796 #[allow(clippy::too_many_lines)] fn try_incremental_text_relayout(
6811 &self,
6812 content: &[InlineContent],
6813 constraints: &UnifiedConstraints,
6814 cached: &solver3::layout_tree::CachedInlineLayout,
6815 edited_node_id: NodeId,
6816 ) -> Option<(UnifiedLayout, bool)> {
6817 use crate::text3::cache::{
6818 try_incremental_relayout as decide_incremental,
6819 IncrementalRelayoutResult, PositionedItem, ShapedItem,
6820 };
6821
6822 let (logical_items, shaped_items) = self.shape_text_for_relayout(content, constraints)?;
6823
6824 if logical_items.is_empty() {
6825 return Some((
6826 UnifiedLayout {
6827 items: Vec::new(),
6828 overflow: crate::text3::cache::OverflowInfo::default(),
6829 },
6830 true,
6831 ));
6832 }
6833
6834 let incremental_ok = cached.line_breaks.is_some()
6841 && cached.layout.overflow.overflow_items.is_empty()
6842 && shaped_items.len() == cached.layout.items.len();
6843
6844 if incremental_ok {
6845 let line_breaks = cached.line_breaks.as_ref().unwrap();
6846
6847 let old_advances: Vec<f32> =
6848 cached.item_metrics.iter().map(|m| m.advance_width).collect();
6849 let new_advances: Vec<f32> =
6850 shaped_items.iter().map(|si| si.bounds().width).collect();
6851
6852 let mut dirty_indices: Vec<usize> = Vec::new();
6857 for (i, (old_a, new_a)) in old_advances.iter().zip(new_advances.iter()).enumerate() {
6858 if (new_a - old_a).abs() > 0.01 {
6859 dirty_indices.push(i);
6860 }
6861 }
6862 for (i, si) in shaped_items.iter().enumerate() {
6863 if let ShapedItem::Cluster(c) = si {
6864 if c.source_node_id == Some(edited_node_id)
6865 && !dirty_indices.contains(&i)
6866 {
6867 dirty_indices.push(i);
6868 }
6869 }
6870 }
6871 dirty_indices.sort_unstable();
6872 dirty_indices.dedup();
6873
6874 let decision =
6875 decide_incremental(&dirty_indices, &old_advances, &new_advances, line_breaks);
6876
6877 match decision {
6878 IncrementalRelayoutResult::GlyphSwap => {
6879 let items: Vec<PositionedItem> = cached
6883 .layout
6884 .items
6885 .iter()
6886 .zip(shaped_items)
6887 .map(|(old_positioned, new_shaped)| PositionedItem {
6888 item: new_shaped,
6889 position: old_positioned.position,
6890 line_index: old_positioned.line_index,
6891 })
6892 .collect();
6893 return Some((
6894 UnifiedLayout {
6895 items,
6896 overflow: cached.layout.overflow.clone(),
6897 },
6898 true,
6899 ));
6900 }
6901 IncrementalRelayoutResult::LineShift {
6902 affected_item,
6903 delta,
6904 } => {
6905 let affected_line = cached.layout.items[affected_item].line_index;
6909 let items: Vec<PositionedItem> = cached
6910 .layout
6911 .items
6912 .iter()
6913 .zip(shaped_items)
6914 .enumerate()
6915 .map(|(i, (old_positioned, new_shaped))| {
6916 let mut position = old_positioned.position;
6917 if i > affected_item && old_positioned.line_index == affected_line {
6918 position.x += delta;
6919 }
6920 PositionedItem {
6921 item: new_shaped,
6922 position,
6923 line_index: old_positioned.line_index,
6924 }
6925 })
6926 .collect();
6927 return Some((
6928 UnifiedLayout {
6929 items,
6930 overflow: cached.layout.overflow.clone(),
6931 },
6932 true,
6933 ));
6934 }
6935 IncrementalRelayoutResult::PartialReflow { .. }
6936 | IncrementalRelayoutResult::FullRelayout => {
6937 }
6939 }
6940 }
6941
6942 let layout = self.fragment_layout_from_shaped(&logical_items, &shaped_items, constraints)?;
6946 Some((layout, false))
6947 }
6948
6949 #[cfg(feature = "a11y")]
6951 fn get_node_used_size_a11y(
6952 &self,
6953 dom_id: DomId,
6954 node_id: NodeId,
6955 ) -> Option<LogicalSize> {
6956 let layout_result = self.layout_results.get(&dom_id)?;
6957 let layout_indices = layout_result.layout_tree.dom_to_layout.get(&node_id)?;
6958 let idx = *layout_indices.first()?;
6959 let node = layout_result.layout_tree.get(idx)?;
6960 node.used_size
6961 }
6962
6963 #[allow(clippy::cast_possible_truncation)] pub fn get_node_bounds(
6966 &self,
6967 dom_id: DomId,
6968 node_id: NodeId,
6969 ) -> Option<azul_css::props::basic::LayoutRect> {
6970 use azul_css::props::basic::LayoutRect;
6971
6972 let layout_result = self.layout_results.get(&dom_id)?;
6973 let layout_indices = layout_result.layout_tree.dom_to_layout.get(&node_id)?;
6974 let idx = *layout_indices.first()?;
6975 let node = layout_result.layout_tree.get(idx)?;
6976
6977 let size = node.used_size?;
6979
6980 let position = layout_result.calculated_positions.get(idx)?;
6982
6983 Some(LayoutRect {
6984 origin: azul_css::props::basic::LayoutPoint {
6985 x: position.x as isize,
6986 y: position.y as isize,
6987 },
6988 size: azul_css::props::basic::LayoutSize {
6989 width: size.width as isize,
6990 height: size.height as isize,
6991 },
6992 })
6993 }
6994
6995 #[cfg(feature = "a11y")]
6997 #[allow(clippy::cast_precision_loss)] fn scroll_to_node_if_needed(
6999 &mut self,
7000 dom_id: DomId,
7001 node_id: NodeId,
7002 now: Instant,
7003 ) {
7004 let Some(target_bounds) = self.get_node_bounds(dom_id, node_id) else {
7006 return;
7007 };
7008
7009 let dom_node_id = DomNodeId {
7011 dom: dom_id,
7012 node: NodeHierarchyItemId::from_crate_internal(Some(node_id)),
7013 };
7014 let Some(scroll_ancestor) = self.find_scrollable_ancestor(dom_node_id) else {
7015 return;
7016 };
7017 let Some(scroll_node_id) = scroll_ancestor.node.into_crate_internal() else {
7018 return;
7019 };
7020 let Some(ancestor_bounds) = self.get_node_bounds(dom_id, scroll_node_id) else {
7021 return;
7022 };
7023
7024 let current_scroll = self
7025 .scroll_manager
7026 .get_current_offset(dom_id, scroll_node_id)
7027 .unwrap_or_default();
7028
7029 let vp_x = ancestor_bounds.origin.x as f32 + current_scroll.x;
7031 let vp_y = ancestor_bounds.origin.y as f32 + current_scroll.y;
7032 let vp_w = ancestor_bounds.size.width as f32;
7033 let vp_h = ancestor_bounds.size.height as f32;
7034
7035 let target_x = target_bounds.origin.x as f32;
7036 let target_y = target_bounds.origin.y as f32;
7037 let target_w = target_bounds.size.width as f32;
7038 let target_h = target_bounds.size.height as f32;
7039
7040 let visible_x = target_x >= vp_x && (target_x + target_w) <= (vp_x + vp_w);
7041 let visible_y = target_y >= vp_y && (target_y + target_h) <= (vp_y + vp_h);
7042
7043 if visible_x && visible_y {
7044 return; }
7046
7047 let mut scroll_x = current_scroll.x;
7049 let mut scroll_y = current_scroll.y;
7050
7051 if target_x < vp_x {
7052 scroll_x = target_x - ancestor_bounds.origin.x as f32;
7053 } else if (target_x + target_w) > (vp_x + vp_w) {
7054 scroll_x = (target_x + target_w) - ancestor_bounds.origin.x as f32 - vp_w;
7055 }
7056
7057 if target_y < vp_y {
7058 scroll_y = target_y - ancestor_bounds.origin.y as f32;
7059 } else if (target_y + target_h) > (vp_y + vp_h) {
7060 scroll_y = (target_y + target_h) - ancestor_bounds.origin.y as f32 - vp_h;
7061 }
7062
7063 self.scroll_manager.scroll_to(
7064 dom_id,
7065 scroll_node_id,
7066 LogicalPosition { x: scroll_x, y: scroll_y },
7067 std::time::Duration::from_millis(300).into(),
7068 EasingFunction::EaseOut,
7069 now,
7070 );
7071 }
7072
7073 #[allow(clippy::cast_precision_loss)] fn scroll_cursor_into_view_if_needed(
7087 &mut self,
7088 dom_id: DomId,
7089 node_id: NodeId,
7090 now: Instant,
7091 ) {
7092 let Some(cursor) = self.text_edit_manager.get_primary_cursor() else {
7094 return;
7095 };
7096
7097 let Some(inline_layout) = self.get_node_inline_layout(dom_id, node_id) else {
7099 return;
7100 };
7101
7102 let Some(cursor_rect) = inline_layout.get_cursor_rect(&cursor) else {
7104 return;
7105 };
7106
7107 let Some(node_bounds) = self.get_node_bounds(dom_id, node_id) else {
7109 return;
7110 };
7111
7112 let cursor_abs_x = node_bounds.origin.x as f32 + cursor_rect.origin.x;
7114 let cursor_abs_y = node_bounds.origin.y as f32 + cursor_rect.origin.y;
7115
7116 let dom_node_id = DomNodeId {
7118 dom: dom_id,
7119 node: NodeHierarchyItemId::from_crate_internal(Some(node_id)),
7120 };
7121 let Some(scroll_ancestor) = self.find_scrollable_ancestor(dom_node_id) else {
7122 return; };
7124 let Some(scroll_node_id) = scroll_ancestor.node.into_crate_internal() else {
7125 return;
7126 };
7127
7128 let Some(ancestor_bounds) = self.get_node_bounds(dom_id, scroll_node_id) else {
7130 return;
7131 };
7132 let current_scroll = self
7133 .scroll_manager
7134 .get_current_offset(dom_id, scroll_node_id)
7135 .unwrap_or_default();
7136
7137 let viewport_x = ancestor_bounds.origin.x as f32 + current_scroll.x;
7139 let viewport_y = ancestor_bounds.origin.y as f32 + current_scroll.y;
7140 let viewport_width = ancestor_bounds.size.width as f32;
7141 let viewport_height = ancestor_bounds.size.height as f32;
7142
7143 let cursor_visible_x = cursor_abs_x >= viewport_x
7145 && cursor_abs_x <= viewport_x + viewport_width;
7146 let cursor_visible_y = cursor_abs_y >= viewport_y
7147 && cursor_abs_y <= viewport_y + viewport_height;
7148
7149 if cursor_visible_x && cursor_visible_y {
7150 return;
7152 }
7153
7154 let mut target_scroll_x = current_scroll.x;
7156 let mut target_scroll_y = current_scroll.y;
7157
7158 if cursor_abs_x < viewport_x {
7160 target_scroll_x = cursor_abs_x - ancestor_bounds.origin.x as f32;
7161 } else if cursor_abs_x > viewport_x + viewport_width {
7162 target_scroll_x = cursor_abs_x - ancestor_bounds.origin.x as f32 - viewport_width
7163 + cursor_rect.size.width;
7164 }
7165
7166 if cursor_abs_y < viewport_y {
7168 target_scroll_y = cursor_abs_y - ancestor_bounds.origin.y as f32;
7169 } else if cursor_abs_y > viewport_y + viewport_height {
7170 target_scroll_y = cursor_abs_y - ancestor_bounds.origin.y as f32 - viewport_height
7171 + cursor_rect.size.height;
7172 }
7173
7174 self.scroll_manager.scroll_to(
7176 dom_id,
7177 scroll_node_id,
7178 LogicalPosition {
7179 x: target_scroll_x,
7180 y: target_scroll_y,
7181 },
7182 std::time::Duration::from_millis(200).into(),
7183 EasingFunction::EaseOut,
7184 now,
7185 );
7186 }
7187
7188 #[allow(clippy::cast_possible_truncation)] fn byte_offset_to_cursor(
7204 text_layout: &UnifiedLayout,
7205 byte_offset: u32,
7206 ) -> TextCursor {
7207 if byte_offset == 0 {
7209 for item in &text_layout.items {
7211 if let ShapedItem::Cluster(cluster) = &item.item {
7212 return TextCursor {
7213 cluster_id: cluster.source_cluster_id,
7214 affinity: CursorAffinity::Trailing,
7215 };
7216 }
7217 }
7218 return TextCursor {
7220 cluster_id: GraphemeClusterId {
7221 source_run: 0,
7222 start_byte_in_run: 0,
7223 },
7224 affinity: CursorAffinity::Trailing,
7225 };
7226 }
7227
7228 let mut current_byte_offset = 0u32;
7230
7231 for item in &text_layout.items {
7232 if let ShapedItem::Cluster(cluster) = &item.item {
7233 let cluster_byte_length = cluster.text.len() as u32;
7235 let cluster_end_byte = current_byte_offset + cluster_byte_length;
7236
7237 if byte_offset >= current_byte_offset && byte_offset <= cluster_end_byte {
7239 return TextCursor {
7241 cluster_id: cluster.source_cluster_id,
7242 affinity: CursorAffinity::Trailing,
7243 };
7244 }
7245
7246 current_byte_offset = cluster_end_byte;
7247 }
7248 }
7249
7250 for item in text_layout.items.iter().rev() {
7252 if let ShapedItem::Cluster(cluster) = &item.item {
7253 return TextCursor {
7254 cluster_id: cluster.source_cluster_id,
7255 affinity: CursorAffinity::Trailing,
7256 };
7257 }
7258 }
7259
7260 TextCursor {
7262 cluster_id: GraphemeClusterId {
7263 source_run: 0,
7264 start_byte_in_run: 0,
7265 },
7266 affinity: CursorAffinity::Trailing,
7267 }
7268 }
7269
7270 fn get_node_inline_layout(
7275 &self,
7276 dom_id: DomId,
7277 node_id: NodeId,
7278 ) -> Option<Arc<UnifiedLayout>> {
7279 let layout_tree = self.layout_cache.tree.as_ref()?;
7281
7282 let layout_idx = layout_tree
7284 .nodes
7285 .iter()
7286 .position(|node| node.dom_node_id == Some(node_id))?;
7287
7288 layout_tree.warm(layout_idx)?
7290 .inline_layout_result
7291 .as_ref()
7292 .map(solver3::layout_tree::CachedInlineLayout::clone_layout)
7293 }
7294
7295 #[must_use = "Returned nodes must be marked dirty for re-layout"]
7311 #[cfg(feature = "a11y")]
7312 #[allow(clippy::match_same_arms)] pub fn edit_text_node(
7314 &mut self,
7315 dom_id: DomId,
7316 node_id: NodeId,
7317 edit_type: &TextEditType,
7318 ) -> Vec<DomNodeId> {
7319 use crate::managers::text_input::TextInputSource;
7320
7321 let text_input = match edit_type {
7323 TextEditType::ReplaceSelection(text) => text.clone(),
7324 TextEditType::SetValue(text) => text.clone(),
7325 TextEditType::SetNumericValue(value) => value.to_string(),
7326 };
7327
7328 let old_inline_content = self.get_text_before_textinput(dom_id, node_id);
7330 let old_text = self.extract_text_from_inline_content(&old_inline_content);
7331
7332 let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(node_id));
7334 let dom_node_id = DomNodeId {
7335 dom: dom_id,
7336 node: hierarchy_id,
7337 };
7338
7339 self.text_input_manager.record_input(
7341 dom_node_id,
7342 text_input,
7343 old_text,
7344 TextInputSource::Accessibility, );
7346
7347 self.apply_text_changeset().dirty_nodes
7349 }
7350
7351 #[cfg(not(feature = "a11y"))]
7352 pub fn process_accessibility_action(
7353 &mut self,
7354 _dom_id: DomId,
7355 _node_id: NodeId,
7356 _action: azul_core::dom::AccessibilityAction,
7357 _now: azul_core::task::Instant,
7358 ) -> BTreeMap<DomNodeId, (Vec<azul_core::events::EventFilter>, bool)> {
7359 BTreeMap::new()
7361 }
7362
7363 #[allow(clippy::too_many_lines)] pub fn process_mouse_click_for_selection(
7392 &mut self,
7393 position: LogicalPosition,
7394 time_ms: u64,
7395 ) -> Option<Vec<DomNodeId>> {
7396 use crate::managers::hover::InputPointId;
7397 use crate::text3::selection::{select_paragraph_at_cursor, select_word_at_cursor};
7398
7399 let mut found_selection: Option<(DomId, NodeId, SelectionRange, LogicalPosition)> = None;
7403
7404 if let Some(hit_test) = self.hover_manager.get_current(&InputPointId::Mouse) {
7406 for (dom_id, hit) in &hit_test.hovered_nodes {
7408 let Some(layout_result) = self.layout_results.get(dom_id) else {
7409 continue;
7410 };
7411 let tree = &layout_result.layout_tree;
7413
7414 let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
7418 let get_dom_depth = |node_id: &NodeId| -> usize {
7419 let mut depth = 0;
7420 let mut current = *node_id;
7421 while let Some(parent) = node_hierarchy.get(current).and_then(azul_core::styled_dom::NodeHierarchyItem::parent_id) {
7422 depth += 1;
7423 current = parent;
7424 }
7425 depth
7426 };
7427
7428 let mut sorted_hits: Vec<_> = hit.regular_hit_test_nodes.iter().collect();
7429 sorted_hits.sort_by(|(a_id, _), (b_id, _)| {
7430 let depth_a = get_dom_depth(a_id);
7431 let depth_b = get_dom_depth(b_id);
7432 depth_b.cmp(&depth_a).then_with(|| a_id.index().cmp(&b_id.index()))
7435 });
7436
7437 for (node_id, hit_item) in sorted_hits {
7438 if !Self::is_text_selectable(&layout_result.styled_dom, *node_id) {
7440 continue;
7441 }
7442
7443 let layout_node_idx = tree.nodes.iter().position(|n| n.dom_node_id == Some(*node_id));
7445 let Some(layout_node_idx) = layout_node_idx else {
7446 continue;
7447 };
7448 let Some(warm_node) = tree.warm(layout_node_idx) else {
7449 continue;
7450 };
7451
7452 let (cached_layout, ifc_root_node_id) = if let Some(ref cached) = warm_node.inline_layout_result {
7455 (cached, *node_id)
7457 } else if let Some(ref membership) = warm_node.ifc_membership {
7458 match tree.warm(membership.ifc_root_layout_index) {
7460 Some(ifc_root_warm) => match (ifc_root_warm.inline_layout_result.as_ref(), tree.get(membership.ifc_root_layout_index).and_then(|n| n.dom_node_id)) {
7461 (Some(cached), Some(root_dom_id)) => (cached, root_dom_id),
7462 _ => continue,
7463 },
7464 None => continue,
7465 }
7466 } else {
7467 continue;
7469 };
7470
7471 let layout = &cached_layout.layout;
7472
7473 let local_pos = hit_item.point_relative_to_item;
7476
7477 if let Some(cursor) = layout.hittest_cursor(local_pos) {
7479 found_selection = Some((*dom_id, ifc_root_node_id, SelectionRange {
7481 start: cursor,
7482 end: cursor,
7483 }, local_pos));
7484 break;
7485 }
7486 }
7487
7488 if found_selection.is_some() {
7489 break;
7490 }
7491 }
7492 }
7493
7494 if found_selection.is_none() {
7497 for (dom_id, layout_result) in &self.layout_results {
7498 let tree = &layout_result.layout_tree;
7502
7503 for (node_idx, layout_node) in tree.nodes.iter().enumerate() {
7505 let Some(warm) = tree.warm(node_idx) else {
7506 continue;
7507 };
7508 let Some(cached_layout) = warm.inline_layout_result.as_ref() else {
7509 continue; };
7511
7512 let Some(node_id) = layout_node.dom_node_id else {
7513 continue;
7514 };
7515
7516 if !Self::is_text_selectable(&layout_result.styled_dom, node_id) {
7518 continue;
7519 }
7520
7521 let node_pos = layout_result.calculated_positions
7524 .get(node_idx)
7525 .copied()
7526 .unwrap_or_default();
7527
7528 let node_size = layout_node.used_size.unwrap_or_else(|| {
7530 let bounds = cached_layout.layout.bounds();
7531 LogicalSize::new(bounds.width, bounds.height)
7532 });
7533
7534 if position.x < node_pos.x || position.x > node_pos.x + node_size.width ||
7535 position.y < node_pos.y || position.y > node_pos.y + node_size.height {
7536 continue;
7537 }
7538
7539 let local_pos = LogicalPosition {
7541 x: position.x - node_pos.x,
7542 y: position.y - node_pos.y,
7543 };
7544
7545 let layout = &cached_layout.layout;
7546
7547 if let Some(cursor) = layout.hittest_cursor(local_pos) {
7549 found_selection = Some((*dom_id, node_id, SelectionRange {
7550 start: cursor,
7551 end: cursor,
7552 }, local_pos));
7553 break;
7554 }
7555 }
7556
7557 if found_selection.is_some() {
7558 break;
7559 }
7560 }
7561 }
7562
7563 let (dom_id, ifc_root_node_id, initial_range, _local_pos) = found_selection?;
7564
7565 let node_hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(ifc_root_node_id));
7568 let dom_node_id = DomNodeId {
7569 dom: dom_id,
7570 node: node_hierarchy_id,
7571 };
7572
7573 let click_count = self.gesture_drag_manager.detect_click_count();
7576
7577 let final_range = if click_count > 1 {
7579 let layout_result = self.layout_results.get(&dom_id)?;
7581 let tree = &layout_result.layout_tree;
7582
7583 let layout_idx = tree.nodes.iter().position(|n| n.dom_node_id == Some(ifc_root_node_id))?;
7585 let cached_layout = tree.warm(layout_idx)?.inline_layout_result.as_ref()?;
7586 let layout = &cached_layout.layout;
7587
7588 match click_count {
7589 2 => select_word_at_cursor(&initial_range.start, layout.as_ref())
7590 .unwrap_or(initial_range),
7591 3 => select_paragraph_at_cursor(&initial_range.start, layout.as_ref())
7592 .unwrap_or(initial_range),
7593 _ => initial_range,
7594 }
7595 } else {
7596 initial_range
7597 };
7598
7599 let is_contenteditable = self.layout_results.get(&dom_id)
7607 .is_some_and(|lr| {
7608 let node_hierarchy = lr.styled_dom.node_hierarchy.as_container();
7609 let node_data = lr.styled_dom.node_data.as_ref();
7610
7611 let mut current_node = Some(ifc_root_node_id);
7613 while let Some(node_id) = current_node {
7614 if let Some(styled_node) = node_data.get(node_id.index()) {
7615 if styled_node.is_contenteditable() {
7619 return true;
7620 }
7621
7622 let has_contenteditable_attr = styled_node.attributes().as_ref().iter().any(|attr| {
7624 matches!(attr, AttributeType::ContentEditable(_))
7625 });
7626 if has_contenteditable_attr {
7627 return true;
7628 }
7629 }
7630 current_node = node_hierarchy.get(node_id).and_then(azul_core::styled_dom::NodeHierarchyItem::parent_id);
7632 }
7633 false
7634 });
7635
7636 let ce_key = self.layout_results.get(&dom_id).map_or(0, |lr| {
7644 azul_core::diff::calculate_contenteditable_key(
7645 lr.styled_dom.node_data.as_ref(),
7646 lr.styled_dom.node_hierarchy.as_ref(),
7647 ifc_root_node_id,
7648 )
7649 });
7650 self.text_edit_manager.initialize_editing(
7651 final_range.start, dom_id, ifc_root_node_id, ce_key,
7652 );
7653 if click_count > 1 && final_range.start != final_range.end {
7658 if let Some(mc) = self.text_edit_manager.multi_cursor.as_mut() {
7659 mc.set_single_range(final_range);
7660 }
7661 }
7662 let now = Instant::now();
7663 self.text_edit_manager.blink.reset_blink_on_input(now);
7664 self.text_edit_manager.blink.set_blink_timer_active(true);
7665 self.regenerate_display_list_for_dom(dom_id);
7670
7671 Some(vec![dom_node_id])
7673 }
7674
7675 pub fn process_mouse_drag_for_selection(
7692 &mut self,
7693 _start_position: LogicalPosition,
7694 current_position: LogicalPosition,
7695 ) -> Option<Vec<DomNodeId>> {
7696 use azul_core::selection::{Selection, SelectionRange};
7697
7698 let mc = self.text_edit_manager.multi_cursor.as_ref()?;
7703 let anchor = match &mc.get_primary()?.selection {
7704 Selection::Cursor(c) => *c,
7705 Selection::Range(r) => r.start, };
7707 let dom_id = mc.node_id.dom;
7708 let node_id = mc.node_id.node.into_crate_internal()?;
7709 let dom_node_id = mc.node_id;
7710
7711 let layout_result = self.layout_results.get(&dom_id)?;
7713 let tree = &layout_result.layout_tree;
7714 let layout_idx = tree.nodes.iter()
7715 .position(|n| n.dom_node_id == Some(node_id))?;
7716 let node_pos = layout_result.calculated_positions
7717 .get(layout_idx)
7718 .copied()
7719 .unwrap_or_default();
7720 let cached = tree.warm(layout_idx)?.inline_layout_result.as_ref()?;
7721
7722 let local_pos = LogicalPosition {
7723 x: current_position.x - node_pos.x,
7724 y: current_position.y - node_pos.y,
7725 };
7726 let focus = cached.layout.hittest_cursor(local_pos)?;
7727
7728 let mc = self.text_edit_manager.multi_cursor.as_mut()?;
7730 if let Some(primary) = mc.get_primary_mut() {
7731 if anchor == focus {
7732 primary.selection = Selection::Cursor(anchor);
7733 } else {
7734 primary.selection = Selection::Range(SelectionRange {
7735 start: anchor,
7736 end: focus,
7737 });
7738 }
7739 }
7740
7741 self.text_edit_manager.mark_dirty();
7742 self.regenerate_display_list_for_dom(dom_id);
7743 Some(vec![dom_node_id])
7744 }
7745
7746 pub fn delete_selection(
7760 &mut self,
7761 target: DomNodeId,
7762 forward: bool,
7763 ) -> Option<Vec<DomNodeId>> {
7764 let dom_id = target.dom;
7765 let node_id = target.node.into_crate_internal()?;
7766
7767 let current_selections = if let Some(ref mc) = self.text_edit_manager.multi_cursor {
7769 mc.to_selections()
7770 } else if let Some(cursor) = self.text_edit_manager.get_primary_cursor() {
7771 vec![Selection::Cursor(cursor)]
7772 } else {
7773 return None;
7774 };
7775
7776 let content = self.get_text_before_textinput(dom_id, node_id);
7777 let edit = if forward {
7778 crate::text3::edit::TextEdit::DeleteForward
7779 } else {
7780 crate::text3::edit::TextEdit::DeleteBackward
7781 };
7782 let (new_content, new_selections) = crate::text3::edit::edit_text(
7783 &content, ¤t_selections, &edit,
7784 );
7785
7786 {
7794 use crate::managers::changeset::{TextChangeset, TextOpDeleteText, TextOperation};
7795 use crate::managers::undo_redo::NodeStateSnapshot;
7796 static DELETE_CHANGESET_COUNTER: AtomicUsize = AtomicUsize::new(0);
7797
7798 let pre_text = self.extract_text_from_inline_content(&content);
7799 let old_cursor = current_selections.first().and_then(|sel| match sel {
7800 Selection::Cursor(c) => Some(*c),
7801 Selection::Range(_) => None,
7802 });
7803 let old_range = current_selections.first().and_then(|sel| match sel {
7804 Selection::Range(r) => Some(*r),
7805 Selection::Cursor(_) => None,
7806 });
7807 let record_range = old_range.unwrap_or_else(|| {
7808 let anchor = old_cursor.unwrap_or(TextCursor {
7809 cluster_id: GraphemeClusterId {
7810 source_run: 0,
7811 start_byte_in_run: 0,
7812 },
7813 affinity: CursorAffinity::Leading,
7814 });
7815 SelectionRange {
7816 start: anchor,
7817 end: anchor,
7818 }
7819 });
7820 let changeset_id =
7821 usize::MAX - DELETE_CHANGESET_COUNTER.fetch_add(1, Ordering::SeqCst);
7822 let timestamp = {
7823 #[cfg(feature = "std")]
7824 {
7825 Instant::now()
7826 }
7827 #[cfg(not(feature = "std"))]
7828 {
7829 azul_core::task::Instant::Tick(azul_core::task::SystemTick {
7830 tick_counter: 0,
7831 })
7832 }
7833 };
7834 let pre_state = NodeStateSnapshot {
7835 node_id,
7836 text_content: pre_text.into(),
7837 cursor_position: old_cursor.into(),
7838 selection_range: old_range.into(),
7839 timestamp: timestamp.clone(),
7840 };
7841 let changeset = TextChangeset {
7842 id: changeset_id,
7843 target,
7844 operation: TextOperation::DeleteText(TextOpDeleteText {
7845 range: record_range,
7846 deleted_text: "".into(),
7847 new_cursor: CursorPosition::Uninitialized,
7848 }),
7849 timestamp,
7850 };
7851 self.undo_redo_manager.store_content_snapshot(
7852 changeset_id,
7853 content,
7854 new_content.clone(),
7855 );
7856 self.undo_redo_manager.record_operation(changeset, pre_state);
7857 }
7858
7859 if let Some(ref mut mc) = self.text_edit_manager.multi_cursor {
7861 mc.update_from_edit_result(&new_selections);
7862 }
7863 self.update_text_cache_after_edit(dom_id, node_id, new_content);
7866 self.regenerate_display_list_for_dom(dom_id);
7867
7868 Some(vec![target])
7869 }
7870
7871 pub fn get_selected_content_for_clipboard(
7887 &self,
7888 dom_id: &DomId,
7889 ) -> Option<crate::managers::selection::ClipboardContent> {
7890 use crate::managers::selection::ClipboardContent;
7891 use crate::text3::edit::cursor_byte_offset_in_run;
7892
7893 let mc = self.text_edit_manager.multi_cursor.as_ref()?;
7894 let node_id = mc.node_id.node.into_crate_internal()?;
7895
7896 let ranges: Vec<_> = mc.selections.iter().filter_map(|s| match &s.selection {
7898 Selection::Range(r) => Some(*r),
7899 Selection::Cursor(_) => None,
7900 }).collect();
7901 if ranges.is_empty() {
7902 return None;
7903 }
7904
7905 let content = self.get_text_before_textinput(*dom_id, node_id);
7912 let mut plain = String::new();
7913 for r in &ranges {
7914 let sr = r.start.cluster_id.source_run as usize;
7915 let er = r.end.cluster_id.source_run as usize;
7916 if sr == er {
7917 if let Some(InlineContent::Text(run)) = content.get(sr) {
7918 let a = cursor_byte_offset_in_run(&run.text, &r.start);
7919 let b = cursor_byte_offset_in_run(&run.text, &r.end);
7920 let (lo, hi) = (a.min(b), a.max(b));
7921 if hi <= run.text.len() && lo < hi {
7922 plain.push_str(&run.text[lo..hi]);
7923 }
7924 }
7925 } else {
7926 let (first_idx, first_cur, last_idx, last_cur) = if sr <= er {
7929 (sr, r.start, er, r.end)
7930 } else {
7931 (er, r.end, sr, r.start)
7932 };
7933 for ri in first_idx..=last_idx {
7934 if let Some(InlineContent::Text(run)) = content.get(ri) {
7935 if ri == first_idx {
7936 let off = cursor_byte_offset_in_run(&run.text, &first_cur).min(run.text.len());
7937 plain.push_str(&run.text[off..]);
7938 } else if ri == last_idx {
7939 let off = cursor_byte_offset_in_run(&run.text, &last_cur).min(run.text.len());
7940 plain.push_str(&run.text[..off]);
7941 } else {
7942 plain.push_str(&run.text);
7943 }
7944 }
7945 }
7946 }
7947 }
7948
7949 if plain.is_empty() {
7950 return None;
7951 }
7952 Some(ClipboardContent {
7953 plain_text: plain.into(),
7954 styled_runs: Vec::new().into(),
7960 })
7961 }
7962
7963 #[allow(clippy::cast_possible_truncation)] #[allow(clippy::too_many_lines)] pub fn process_image_callback_updates(
7980 &mut self,
7981 image_callbacks_changed: &BTreeMap<DomId, FastBTreeSet<NodeId>>,
7982 gl_context: &OptionGlContextPtr,
7983 ) -> Vec<(DomId, NodeId, azul_core::gl::Texture)> {
7984 use crate::callbacks::{RenderImageCallback, RenderImageCallbackInfo};
7985 use std::panic;
7986
7987 let mut updated_textures = Vec::new();
7988
7989 for (dom_id, node_ids) in image_callbacks_changed {
7990 let Some(layout_result) = self.layout_results.get_mut(dom_id) else {
7991 continue;
7992 };
7993
7994 for node_id in node_ids {
7995 let node_data_container = layout_result.styled_dom.node_data.as_container();
7997 let Some(node_data) = node_data_container.get(*node_id) else {
7998 continue;
7999 };
8000
8001 let has_callback = matches!(node_data.get_node_type(), NodeType::Image(img_ref)
8003 if img_ref.get_image_callback().is_some());
8004
8005 if !has_callback {
8006 continue;
8007 }
8008
8009 let layout_indices = match layout_result.layout_tree.dom_to_layout.get(node_id) {
8012 Some(indices) if !indices.is_empty() => indices,
8013 _ => continue,
8014 };
8015
8016 let layout_index = layout_indices[0];
8018
8019 let position = match layout_result.calculated_positions.get(layout_index) {
8021 Some(pos) => *pos,
8022 None => continue,
8023 };
8024
8025 let Some(layout_node) = layout_result.layout_tree.get(layout_index) else {
8027 continue;
8028 };
8029
8030 let (width, height) = match layout_node.used_size {
8032 Some(size) => (size.width, size.height),
8033 None => continue, };
8035
8036 let callback_domnode_id = DomNodeId {
8037 dom: *dom_id,
8038 node: NodeHierarchyItemId::from_crate_internal(Some(
8039 *node_id,
8040 )),
8041 };
8042
8043 let bounds = HidpiAdjustedBounds::from_bounds(
8044 azul_css::props::basic::LayoutSize {
8045 width: width as isize,
8046 height: height as isize,
8047 },
8048 self.current_window_state.size.get_hidpi_factor(),
8049 );
8050
8051 let mut gl_callback_info = RenderImageCallbackInfo::new(
8053 callback_domnode_id,
8054 bounds,
8055 gl_context,
8056 &self.image_cache,
8057 &self.font_manager.fc_cache,
8058 );
8059
8060 let new_image_ref = {
8062 let mut node_data_mut = layout_result.styled_dom.node_data.as_container_mut();
8063 match node_data_mut.get_mut(*node_id) {
8064 Some(nd) => {
8065 match &mut nd.node_type {
8066 NodeType::Image(ref mut img_ref) => {
8067 let callback_result = img_ref.as_mut().get_image_callback_mut();
8069
8070 if callback_result.is_none() {
8071 match img_ref.get_data() {
8075 azul_core::resources::DecodedImage::Callback(core_callback) => {
8076 if core_callback.callback.cb == 0 {
8077 None
8078 } else {
8079 let callback = RenderImageCallback::from_core(&core_callback.callback);
8080 let refany_clone = core_callback.refany.clone();
8081 let result = panic::catch_unwind(panic::AssertUnwindSafe(|| {
8082 (callback.cb)(refany_clone, gl_callback_info)
8083 }));
8084 result.ok()
8085 }
8086 }
8087 _ => None,
8088 }
8089 } else {
8090 callback_result.map(|core_callback| {
8091 let callback =
8094 RenderImageCallback::from_core(&core_callback.callback);
8095 (callback.cb)(
8096 core_callback.refany.clone(),
8097 gl_callback_info,
8098 )
8099 })
8100 }
8101 }
8102 _ => None,
8103 }
8104 }
8105 None => None,
8106 }
8107 };
8108
8109 #[cfg(feature = "gl_context_loader")]
8111 if let Some(gl) = gl_context.as_ref() {
8112 use gl_context_loader::gl;
8113 gl.bind_framebuffer(gl::FRAMEBUFFER, 0);
8114 gl.disable(gl::FRAMEBUFFER_SRGB);
8115 gl.disable(gl::MULTISAMPLE);
8116 }
8117
8118 if let Some(image_ref) = new_image_ref {
8120 if let Some(azul_core::resources::DecodedImage::Gl(texture)) = image_ref.into_inner() {
8121 updated_textures.push((*dom_id, *node_id, texture));
8122 }
8123 }
8124 }
8125 }
8126
8127 updated_textures
8128 }
8129
8130 pub fn check_and_queue_virtual_view_reinvoke(
8139 &mut self,
8140 dom_id: DomId,
8141 node_id: NodeId,
8142 ) -> bool {
8143 let Some(bounds) = Self::get_virtual_view_bounds_from_layout(
8145 &self.layout_results,
8146 dom_id,
8147 node_id,
8148 ) else {
8149 return false; };
8151
8152 let reason = self.virtual_view_manager.check_reinvoke(
8154 dom_id, node_id, &self.scroll_manager, bounds,
8155 );
8156
8157 if let Some(reason) = reason {
8158 self.pending_virtual_view_updates
8161 .entry(dom_id)
8162 .or_default()
8163 .insert(node_id, reason);
8164 true
8165 } else {
8166 false
8167 }
8168 }
8169
8170 pub fn process_virtual_view_updates(
8187 &mut self,
8188 vviews_to_update: &BTreeMap<DomId, BTreeMap<NodeId, VirtualViewCallbackReason>>,
8189 window_state: &FullWindowState,
8190 renderer_resources: &RendererResources,
8191 system_callbacks: &ExternalSystemCallbacks,
8192 ) -> Vec<(DomId, NodeId)> {
8193 let mut updated_vviews = Vec::new();
8194
8195 for (dom_id, node_ids) in vviews_to_update {
8196 for (node_id, reason) in node_ids {
8197 let Some(bounds) = Self::get_virtual_view_bounds_from_layout(
8199 &self.layout_results,
8200 *dom_id,
8201 *node_id,
8202 ) else {
8203 continue;
8204 };
8205
8206 self.virtual_view_manager
8211 .set_reason_override(*dom_id, *node_id, *reason);
8212
8213 if let Some(_child_dom_id) = self.invoke_virtual_view_callback(
8215 *dom_id,
8216 *node_id,
8217 bounds,
8218 window_state,
8219 renderer_resources,
8220 system_callbacks,
8221 &mut None,
8222 ) {
8223 updated_vviews.push((*dom_id, *node_id));
8224 }
8225 }
8226 }
8227
8228 updated_vviews
8229 }
8230
8231 pub fn queue_virtual_view_updates(
8235 &mut self,
8236 vviews_to_update: BTreeMap<DomId, FastBTreeSet<NodeId>>,
8237 ) {
8238 for (dom_id, node_ids) in vviews_to_update {
8245 let entry = self.pending_virtual_view_updates.entry(dom_id).or_default();
8246 for node_id in node_ids {
8247 entry
8248 .entry(node_id)
8249 .or_insert(VirtualViewCallbackReason::DomRecreated);
8250 }
8251 }
8252 }
8253
8254 pub fn queue_all_virtual_view_reinvoke(&mut self) {
8263 let mut updates: BTreeMap<DomId, FastBTreeSet<NodeId>> = BTreeMap::new();
8264 for (dom_id, node_id) in self.virtual_view_manager.all_view_keys() {
8265 updates
8266 .entry(dom_id)
8267 .or_default()
8268 .insert(node_id);
8269 }
8270 self.queue_virtual_view_updates(updates);
8271 }
8272
8273 pub fn process_pending_virtual_view_updates(
8277 &mut self,
8278 window_state: &FullWindowState,
8279 renderer_resources: &RendererResources,
8280 system_callbacks: &ExternalSystemCallbacks,
8281 ) -> Vec<(DomId, NodeId)> {
8282 if self.pending_virtual_view_updates.is_empty() {
8283 return Vec::new();
8284 }
8285
8286 let vviews_to_update = core::mem::take(&mut self.pending_virtual_view_updates);
8288
8289 let updated = self.process_virtual_view_updates(
8291 &vviews_to_update,
8292 window_state,
8293 renderer_resources,
8294 system_callbacks,
8295 );
8296
8297 for (parent_dom, node_id) in &updated {
8304 if let Some(child_dom) = self
8305 .virtual_view_manager
8306 .get_nested_dom_id(*parent_dom, *node_id)
8307 {
8308 self.hover_manager.purge_dom(&child_dom);
8309 }
8310 }
8311
8312 updated
8313 }
8314
8315 fn get_virtual_view_bounds_from_layout(
8319 layout_results: &BTreeMap<DomId, DomLayoutResult>,
8320 dom_id: DomId,
8321 node_id: NodeId,
8322 ) -> Option<LogicalRect> {
8323 let layout_result = layout_results.get(&dom_id)?;
8324
8325 let node_data_container = layout_result.styled_dom.node_data.as_container();
8327 let node_data = node_data_container.get(node_id)?;
8328
8329 if !matches!(node_data.get_node_type(), NodeType::VirtualView) {
8330 return None;
8331 }
8332
8333 let layout_indices = layout_result.layout_tree.dom_to_layout.get(&node_id)?;
8335 if layout_indices.is_empty() {
8336 return None;
8337 }
8338
8339 let layout_index = layout_indices[0];
8340
8341 let position = *layout_result.calculated_positions.get(layout_index)?;
8343
8344 let layout_node = layout_result.layout_tree.get(layout_index)?;
8346 let size = layout_node.used_size?;
8347
8348 Some(LogicalRect::new(
8349 position,
8350 LogicalSize::new(size.width, size.height),
8351 ))
8352 }
8353}
8354
8355#[cfg(feature = "a11y")]
8356#[derive(Debug, Clone)]
8357pub enum TextEditType {
8358 ReplaceSelection(String),
8359 SetValue(String),
8360 SetNumericValue(f64),
8361}
8362
8363impl LayoutWindow {
8368 #[allow(clippy::too_many_lines)]
8389 pub fn remap_node_ids(&mut self, dom: DomId, map: &crate::managers::NodeIdMap) {
8390 use crate::managers::NodeIdRemap;
8391
8392 let Self {
8393 scroll_manager,
8395 gesture_drag_manager,
8396 focus_manager,
8397 text_edit_manager,
8398 hover_manager,
8399 virtual_view_manager,
8400 gpu_state_manager,
8401 text_input_manager,
8402 undo_redo_manager,
8403 permission_manager,
8404
8405 text_constraints_cache,
8407 dirty_text_nodes,
8408 pending_virtual_view_updates,
8409 gl_texture_cache,
8410 currently_dragging_thumb,
8411
8412 frame_report: _,
8416 frame_report_reset_request: _,
8417 layout_cache: _,
8418 layout_results: _,
8419 text_cache: _,
8421 font_manager: _,
8422 image_cache: _,
8423 cpu_image_callback_results: _,
8424 renderer_resources: _,
8425 a11y_manager: _,
8428 geolocation_manager: _,
8431 biometric_manager: _,
8432 keyring_manager: _,
8433 sensor_manager: _,
8434 gamepad_manager: _,
8435 file_drop_manager: _,
8437 clipboard_manager: _,
8438 skip_gpu_sync: _,
8441 e2e_mount: _,
8442 #[cfg(feature = "e2e-server")]
8443 e2e_scratch: _,
8444 #[cfg(feature = "pdf")]
8445 fragmentation_context: _,
8446 safe_area_insets: _,
8447 timers: _,
8448 threads: _,
8449 renderer_type: _,
8450 previous_window_state: _,
8451 current_window_state: _,
8452 document_id: _,
8453 id_namespace: _,
8454 epoch: _,
8455 system_style: _,
8456 monitors: _,
8457 font_stacks_hash: _,
8458 pre_preedit_content: _,
8459 input_interpreter: _,
8460 post_filter: _,
8461 routes: _,
8462 #[cfg(feature = "icu")]
8463 icu_localizer: _,
8464 pending_lifecycle_events: _,
8470 pending_unmount_invocations: _,
8471 } = self;
8472
8473 scroll_manager.remap_node_ids(dom, map);
8474 gesture_drag_manager.remap_node_ids(dom, map);
8475 focus_manager.remap_node_ids(dom, map);
8476 text_edit_manager.remap_node_ids(dom, map);
8477 hover_manager.remap_node_ids(dom, map);
8478 virtual_view_manager.remap_node_ids(dom, map);
8479 gpu_state_manager.remap_node_ids(dom, map);
8480 text_input_manager.remap_node_ids(dom, map);
8481 undo_redo_manager.remap_node_ids(dom, map);
8482 permission_manager.remap_node_ids(dom, map);
8483
8484 crate::managers::remap_dom_keys(&mut text_constraints_cache.constraints, dom, map);
8486 crate::managers::remap_dom_keys(dirty_text_nodes, dom, map);
8487
8488 if let Some(pending) = pending_virtual_view_updates.remove(&dom) {
8489 let remapped: BTreeMap<NodeId, _> = pending
8490 .into_iter()
8491 .filter_map(|(node_id, reason)| Some((map.resolve(node_id)?, reason)))
8492 .collect();
8493 if !remapped.is_empty() {
8494 pending_virtual_view_updates.insert(dom, remapped);
8495 }
8496 }
8497
8498 if let Some(textures) = gl_texture_cache.solved_textures.remove(&dom) {
8499 let remapped: BTreeMap<NodeId, _> = textures
8500 .into_iter()
8501 .filter_map(|(node_id, tex)| Some((map.resolve(node_id)?, tex)))
8502 .collect();
8503 gl_texture_cache.solved_textures.insert(dom, remapped);
8504 }
8505 let hashes = core::mem::take(&mut gl_texture_cache.hashes);
8506 gl_texture_cache.hashes = hashes
8507 .into_iter()
8508 .filter_map(|((d, node_id, image_hash), v)| {
8509 if d != dom {
8510 return Some(((d, node_id, image_hash), v));
8511 }
8512 Some(((d, map.resolve(node_id)?, image_hash), v))
8513 })
8514 .collect();
8515
8516 if let Some(drag) = currently_dragging_thumb.as_ref() {
8519 match remap_scrollbar_hit_id(drag.hit_id, dom, map) {
8520 Some(new_id) => {
8521 if let Some(d) = currently_dragging_thumb.as_mut() {
8522 d.hit_id = new_id;
8523 }
8524 }
8525 None => *currently_dragging_thumb = None,
8526 }
8527 }
8528 }
8529}
8530
8531fn remap_scrollbar_hit_id(
8534 id: ScrollbarHitId,
8535 dom: DomId,
8536 map: &crate::managers::NodeIdMap,
8537) -> Option<ScrollbarHitId> {
8538 Some(match id {
8539 ScrollbarHitId::VerticalTrack(d, n) if d == dom => {
8540 ScrollbarHitId::VerticalTrack(d, map.resolve(n)?)
8541 }
8542 ScrollbarHitId::VerticalThumb(d, n) if d == dom => {
8543 ScrollbarHitId::VerticalThumb(d, map.resolve(n)?)
8544 }
8545 ScrollbarHitId::HorizontalTrack(d, n) if d == dom => {
8546 ScrollbarHitId::HorizontalTrack(d, map.resolve(n)?)
8547 }
8548 ScrollbarHitId::HorizontalThumb(d, n) if d == dom => {
8549 ScrollbarHitId::HorizontalThumb(d, map.resolve(n)?)
8550 }
8551 other => other,
8552 })
8553}
8554
8555#[cfg(test)]
8563#[allow(clippy::float_cmp, clippy::unreadable_literal)]
8564mod autotest_generated {
8565 use super::*;
8566
8567 fn pos(x: f32, y: f32) -> LogicalPosition {
8572 LogicalPosition::new(x, y)
8573 }
8574
8575 fn size(w: f32, h: f32) -> LogicalSize {
8576 LogicalSize::new(w, h)
8577 }
8578
8579 fn rect(x: f32, y: f32, w: f32, h: f32) -> LogicalRect {
8580 LogicalRect::new(pos(x, y), size(w, h))
8581 }
8582
8583 fn tick(n: u64) -> Instant {
8584 Instant::Tick(azul_core::task::SystemTick { tick_counter: n })
8585 }
8586
8587 fn tick_dur(n: u64) -> Duration {
8588 Duration::Tick(SystemTickDiff { tick_diff: n })
8589 }
8590
8591 fn sys_dur_ms(ms: u64) -> Duration {
8592 Duration::System(SystemTimeDiff::from_millis(ms))
8593 }
8594
8595 fn fresh_window() -> LayoutWindow {
8596 LayoutWindow::new(FcFontCache::default()).expect("LayoutWindow::new must succeed")
8597 }
8598
8599 fn bare_layout_result(styled_dom: StyledDom) -> DomLayoutResult {
8603 DomLayoutResult {
8604 styled_dom,
8605 layout_tree: LayoutTree {
8606 nodes: Vec::new(),
8607 warm: Vec::new(),
8608 cold: Vec::new(),
8609 root: 0,
8610 dom_to_layout: BTreeMap::new(),
8611 children_arena: Vec::new(),
8612 children_offsets: Vec::new(),
8613 subtree_needs_intrinsic: Vec::new(),
8614 },
8615 calculated_positions: Vec::new(),
8616 viewport: LogicalRect::zero(),
8617 display_list: DisplayList::default(),
8618 scroll_ids: HashMap::new(),
8619 scroll_id_to_node_id: HashMap::new(),
8620 }
8621 }
8622
8623 fn fixture_dom() -> StyledDom {
8625 StyledDom::create_from_dom(
8626 Dom::create_body()
8627 .with_child(Dom::create_div())
8628 .with_child(Dom::create_div())
8629 .with_child(Dom::create_div()),
8630 )
8631 }
8632
8633 fn window_with_fixture() -> LayoutWindow {
8634 let mut w = fresh_window();
8635 w.layout_results
8636 .insert(DomId::ROOT_ID, bare_layout_result(fixture_dom()));
8637 w
8638 }
8639
8640 fn dnid(index: usize) -> DomNodeId {
8641 DomNodeId {
8642 dom: DomId::ROOT_ID,
8643 node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(index))),
8644 }
8645 }
8646
8647 fn hostile_node_ids() -> Vec<NodeHierarchyItemId> {
8651 vec![
8652 NodeHierarchyItemId::NONE,
8653 NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(999_999))),
8654 NodeHierarchyItemId::from_raw(usize::MAX),
8657 ]
8658 }
8659
8660 #[test]
8665 fn new_document_id_is_strictly_monotonic_and_carries_a_fresh_namespace() {
8666 let a = new_document_id();
8667 let b = new_document_id();
8668 assert_ne!(a, b, "two DocumentIds must never be equal");
8669 assert!(b.id > a.id, "the counter is fetch_add, so it must increase");
8670 assert_ne!(
8671 a.namespace_id, b.namespace_id,
8672 "each DocumentId burns a fresh IdNamespace"
8673 );
8674 }
8675
8676 #[test]
8677 fn new_id_namespace_is_strictly_monotonic() {
8678 let a = new_id_namespace();
8679 let b = new_id_namespace();
8680 let c = new_id_namespace();
8681 assert!(a.0 < b.0 && b.0 < c.0);
8682 }
8683
8684 #[test]
8689 fn frame_damage_default_is_none() {
8690 assert_eq!(FrameDamage::default(), FrameDamage::None);
8691 assert!(FrameDamage::default().is_none());
8692 assert!(!FrameDamage::default().is_full());
8693 }
8694
8695 #[test]
8696 fn frame_damage_is_none_and_is_full_are_mutually_exclusive() {
8697 let cases = [
8698 FrameDamage::None,
8699 FrameDamage::Full,
8700 FrameDamage::Rects(Vec::new()),
8701 FrameDamage::Rects(vec![rect(0.0, 0.0, 1.0, 1.0)]),
8702 ];
8703 for d in &cases {
8704 assert!(
8705 !(d.is_none() && d.is_full()),
8706 "no variant may be both none and full: {d:?}"
8707 );
8708 }
8709 assert!(FrameDamage::None.is_none());
8710 assert!(!FrameDamage::None.is_full());
8711 assert!(FrameDamage::Full.is_full());
8712 assert!(!FrameDamage::Full.is_none());
8713 assert!(!FrameDamage::Rects(Vec::new()).is_none());
8716 assert!(!FrameDamage::Rects(Vec::new()).is_full());
8717 }
8718
8719 #[test]
8720 fn frame_damage_rect_count_matches_documented_table() {
8721 assert_eq!(FrameDamage::None.rect_count(), 0);
8722 assert_eq!(FrameDamage::Full.rect_count(), 1);
8723 assert_eq!(FrameDamage::Rects(Vec::new()).rect_count(), 0);
8724 assert_eq!(
8725 FrameDamage::Rects(vec![rect(0.0, 0.0, 1.0, 1.0); 3]).rect_count(),
8726 3
8727 );
8728 assert_eq!(
8730 FrameDamage::Rects(vec![LogicalRect::zero(); 4096]).rect_count(),
8731 4096
8732 );
8733 }
8734
8735 #[test]
8736 fn frame_damage_rects_is_some_only_for_the_rects_variant() {
8737 assert!(FrameDamage::None.rects().is_none());
8738 assert!(FrameDamage::Full.rects().is_none());
8739 let empty: &[LogicalRect] = &[];
8740 assert_eq!(FrameDamage::Rects(Vec::new()).rects(), Some(empty));
8741 let r = rect(1.0, 2.0, 3.0, 4.0);
8742 assert_eq!(FrameDamage::Rects(vec![r]).rects(), Some(&[r][..]));
8743 for d in [
8745 FrameDamage::None,
8746 FrameDamage::Full,
8747 FrameDamage::Rects(Vec::new()),
8748 FrameDamage::Rects(vec![r, r]),
8749 ] {
8750 if let Some(slice) = d.rects() {
8751 assert_eq!(slice.len(), d.rect_count());
8752 }
8753 }
8754 }
8755
8756 #[test]
8761 fn frame_damage_area_none_is_always_exactly_zero() {
8762 for window_area in [0.0, 1.0, -1.0, f32::MAX, f32::MIN, f32::INFINITY] {
8763 let a = FrameDamage::None.area(window_area);
8764 assert_eq!(a, 0.0, "None must swallow window_area={window_area}");
8765 }
8766 let a = FrameDamage::None.area(f32::NAN);
8768 assert!(!a.is_nan() && a == 0.0);
8769 }
8770
8771 #[test]
8772 fn frame_damage_area_full_passes_window_area_through_verbatim() {
8773 assert_eq!(FrameDamage::Full.area(0.0), 0.0);
8774 assert_eq!(FrameDamage::Full.area(1920.0 * 1080.0), 2_073_600.0);
8775 assert_eq!(FrameDamage::Full.area(-5.0), -5.0);
8777 assert_eq!(FrameDamage::Full.area(f32::INFINITY), f32::INFINITY);
8778 assert!(FrameDamage::Full.area(f32::NAN).is_nan());
8779 }
8780
8781 #[test]
8782 fn frame_damage_area_of_rects_ignores_window_area_and_sums_products() {
8783 assert_eq!(FrameDamage::Rects(Vec::new()).area(999.0), 0.0);
8784 let d = FrameDamage::Rects(vec![rect(0.0, 0.0, 10.0, 10.0), rect(50.0, 50.0, 2.0, 3.0)]);
8785 assert_eq!(d.area(1.0), 106.0);
8786 assert_eq!(d.area(f32::MAX), 106.0, "window_area is unused for Rects");
8787 }
8788
8789 #[test]
8790 fn frame_damage_area_of_degenerate_rects_is_defined_not_panicking() {
8791 assert_eq!(FrameDamage::Rects(vec![rect(5.0, 5.0, 0.0, 0.0)]).area(1.0), 0.0);
8793 assert_eq!(
8796 FrameDamage::Rects(vec![rect(0.0, 0.0, -10.0, 10.0)]).area(1.0),
8797 -100.0
8798 );
8799 let huge = FrameDamage::Rects(vec![rect(0.0, 0.0, f32::MAX, f32::MAX)]);
8801 assert!(huge.area(1.0).is_infinite() && huge.area(1.0) > 0.0);
8802 assert!(FrameDamage::Rects(vec![rect(0.0, 0.0, f32::NAN, 1.0)])
8804 .area(1.0)
8805 .is_nan());
8806 }
8807
8808 #[test]
8813 fn present_rects_zero_sized_buffer_is_always_none_even_when_forced() {
8814 for d in [
8815 FrameDamage::None,
8816 FrameDamage::Full,
8817 FrameDamage::Rects(vec![rect(0.0, 0.0, 10.0, 10.0)]),
8818 ] {
8819 assert_eq!(d.to_present_rects_physical(1.0, 0, 100, false), None);
8820 assert_eq!(d.to_present_rects_physical(1.0, 100, 0, false), None);
8821 assert_eq!(d.to_present_rects_physical(1.0, 0, 0, true), None);
8822 assert_eq!(d.to_present_rects_physical(1.0, 0, 480, true), None);
8824 }
8825 }
8826
8827 #[test]
8828 fn present_rects_force_full_overrides_every_variant() {
8829 for d in [
8830 FrameDamage::None,
8831 FrameDamage::Full,
8832 FrameDamage::Rects(Vec::new()),
8833 FrameDamage::Rects(vec![rect(1.0, 1.0, 2.0, 2.0)]),
8834 ] {
8835 assert_eq!(
8836 d.to_present_rects_physical(2.0, 640, 480, true),
8837 Some(vec![(0, 0, 640, 480)]),
8838 "force_full must present the whole buffer for {d:?}"
8839 );
8840 }
8841 }
8842
8843 #[test]
8844 fn present_rects_variant_defaults() {
8845 assert_eq!(
8846 FrameDamage::None.to_present_rects_physical(1.0, 800, 600, false),
8847 None,
8848 "None => present nothing"
8849 );
8850 assert_eq!(
8851 FrameDamage::Full.to_present_rects_physical(1.0, 800, 600, false),
8852 Some(vec![(0, 0, 800, 600)])
8853 );
8854 assert_eq!(
8855 FrameDamage::Rects(Vec::new()).to_present_rects_physical(1.0, 800, 600, false),
8856 None,
8857 "an empty rect list is nothing to present"
8858 );
8859 }
8860
8861 #[test]
8862 fn present_rects_round_outward_so_fractional_edges_are_covered() {
8863 let d = FrameDamage::Rects(vec![rect(0.5, 0.25, 1.0, 1.5)]);
8866 assert_eq!(
8867 d.to_present_rects_physical(1.0, 100, 100, false),
8868 Some(vec![(0, 0, 2, 2)])
8869 );
8870 }
8871
8872 #[test]
8873 fn present_rects_apply_the_dpi_factor() {
8874 let d = FrameDamage::Rects(vec![rect(1.0, 1.0, 3.0, 3.0)]);
8875 assert_eq!(
8876 d.to_present_rects_physical(2.0, 100, 100, false),
8877 Some(vec![(2, 2, 6, 6)])
8878 );
8879 }
8880
8881 #[test]
8882 fn present_rects_clamp_to_the_buffer_instead_of_wrapping() {
8883 let d = FrameDamage::Rects(vec![rect(-50.0, -50.0, 100.0, 100.0)]);
8885 assert_eq!(
8886 d.to_present_rects_physical(1.0, 10, 10, false),
8887 Some(vec![(0, 0, 10, 10)])
8888 );
8889 let d = FrameDamage::Rects(vec![rect(1000.0, 1000.0, 10.0, 10.0)]);
8891 assert_eq!(d.to_present_rects_physical(1.0, 100, 100, false), None);
8892 let d = FrameDamage::Rects(vec![rect(f32::MAX, f32::MAX, 10.0, 10.0)]);
8894 assert_eq!(d.to_present_rects_physical(1.0, 100, 100, false), None);
8895 }
8896
8897 #[test]
8898 fn present_rects_drop_degenerate_and_inverted_rects() {
8899 assert_eq!(
8901 FrameDamage::Rects(vec![rect(5.0, 5.0, 0.0, 0.0)])
8902 .to_present_rects_physical(1.0, 100, 100, false),
8903 None
8904 );
8905 assert_eq!(
8907 FrameDamage::Rects(vec![rect(50.0, 50.0, -10.0, -10.0)])
8908 .to_present_rects_physical(1.0, 100, 100, false),
8909 None
8910 );
8911 let d = FrameDamage::Rects(vec![rect(5.0, 5.0, 0.0, 0.0), rect(0.0, 0.0, 4.0, 4.0)]);
8913 assert_eq!(
8914 d.to_present_rects_physical(1.0, 100, 100, false),
8915 Some(vec![(0, 0, 4, 4)])
8916 );
8917 }
8918
8919 #[test]
8920 fn present_rects_collapse_past_sixteen_rects() {
8921 let one = |i: u32| rect(i as f32, 0.0, 1.0, 1.0);
8922 let sixteen = FrameDamage::Rects((0..16).map(one).collect());
8924 let got = sixteen
8925 .to_present_rects_physical(1.0, 100, 100, false)
8926 .expect("16 in-bounds rects must present");
8927 assert_eq!(got.len(), 16);
8928 assert_eq!(got[0], (0, 0, 1, 1));
8929 assert_eq!(got[15], (15, 0, 1, 1));
8930 let seventeen = FrameDamage::Rects((0..17).map(one).collect());
8932 assert_eq!(
8933 seventeen.to_present_rects_physical(1.0, 100, 100, false),
8934 Some(vec![(0, 0, 100, 100)])
8935 );
8936 let many = FrameDamage::Rects((0..4096).map(|i| one(i % 100)).collect());
8938 assert_eq!(
8939 many.to_present_rects_physical(1.0, 100, 100, false),
8940 Some(vec![(0, 0, 100, 100)])
8941 );
8942 }
8943
8944 #[test]
8945 fn present_rects_survive_nan_and_infinite_dpi() {
8946 let d = FrameDamage::Rects(vec![rect(10.0, 10.0, 20.0, 20.0)]);
8947 assert_eq!(d.to_present_rects_physical(f32::NAN, 100, 100, false), None);
8950 assert_eq!(d.to_present_rects_physical(0.0, 100, 100, false), None);
8952 assert_eq!(d.to_present_rects_physical(-1.0, 100, 100, false), None);
8953 assert_eq!(
8954 d.to_present_rects_physical(f32::NEG_INFINITY, 100, 100, false),
8955 None
8956 );
8957 let at_origin = FrameDamage::Rects(vec![rect(0.0, 0.0, 10.0, 10.0)]);
8959 assert_eq!(
8960 at_origin.to_present_rects_physical(f32::INFINITY, 100, 100, false),
8961 Some(vec![(0, 0, 100, 100)])
8962 );
8963 assert_eq!(
8964 at_origin.to_present_rects_physical(f32::MAX, 100, 100, false),
8965 Some(vec![(0, 0, 100, 100)])
8966 );
8967 }
8968
8969 #[test]
8970 fn present_rects_never_escape_the_buffer_for_any_input() {
8971 let buf_w = 137_u32;
8972 let buf_h = 71_u32;
8973 let damages = [
8974 FrameDamage::Full,
8975 FrameDamage::Rects(vec![rect(-1e9, -1e9, 2e9, 2e9)]),
8976 FrameDamage::Rects(vec![rect(0.0, 0.0, f32::INFINITY, f32::INFINITY)]),
8977 FrameDamage::Rects(vec![rect(f32::NAN, 0.0, 10.0, 10.0)]),
8978 FrameDamage::Rects(vec![rect(136.9, 70.9, 0.2, 0.2)]),
8979 FrameDamage::Rects(vec![rect(0.0, 0.0, 1.0, 1.0); 40]),
8980 ];
8981 let dpis = [0.0_f32, 0.5, 1.0, 2.0, 3.5, 1e9, f32::NAN, f32::INFINITY, -2.0];
8982 for d in &damages {
8983 for dpi in dpis {
8984 for force in [false, true] {
8985 if let Some(rects) = d.to_present_rects_physical(dpi, buf_w, buf_h, force) {
8986 assert!(!rects.is_empty(), "Some(..) must never be empty: {d:?}");
8987 for (x, y, w, h) in rects {
8988 assert!(w > 0 && h > 0, "present rects must be non-degenerate");
8989 assert!(
8990 x.checked_add(w).is_some_and(|far| far <= buf_w),
8991 "rect escapes buffer width: {x}+{w} > {buf_w} ({d:?}, dpi={dpi})"
8992 );
8993 assert!(
8994 y.checked_add(h).is_some_and(|far| far <= buf_h),
8995 "rect escapes buffer height: {y}+{h} > {buf_h} ({d:?}, dpi={dpi})"
8996 );
8997 }
8998 }
8999 }
9000 }
9001 }
9002 }
9003
9004 fn synced_report() -> FrameReport {
9011 FrameReport::default()
9012 }
9013
9014 fn record(r: &mut FrameReport, paint: FrameDamage, present: FrameDamage) {
9016 r.record_frame_at_generation(r.reset_generation, paint, present);
9017 }
9018
9019 #[test]
9020 fn frame_report_default_is_all_zero() {
9021 let r = FrameReport::default();
9022 assert_eq!(r.frame_index, 0);
9023 assert_eq!(r.frames_since_reset, 0);
9024 assert_eq!(r.relayout_iterations, 0);
9025 assert_eq!(r.dom_regenerations, 0);
9026 assert_eq!(r.reset_generation, 0);
9027 assert_eq!(r.terminal_result, 0);
9028 assert!(!r.hit_depth_cap);
9029 assert_eq!(r.paint_damage, FrameDamage::None);
9030 assert_eq!(r.present_damage, FrameDamage::None);
9031 assert_eq!(r.accumulated_paint_damage, FrameDamage::None);
9032 assert_eq!(r.accumulated_present_damage, FrameDamage::None);
9033 }
9034
9035 #[test]
9036 fn frame_report_merge_into_full_dominates_and_none_is_neutral() {
9037 let a = rect(0.0, 0.0, 1.0, 1.0);
9038 let b = rect(9.0, 9.0, 2.0, 2.0);
9039
9040 for start in [
9042 FrameDamage::None,
9043 FrameDamage::Full,
9044 FrameDamage::Rects(vec![a]),
9045 ] {
9046 let mut acc = start.clone();
9047 FrameReport::merge_into(&mut acc, &FrameDamage::None);
9048 assert_eq!(acc, start, "merging None must not change the accumulator");
9049 }
9050
9051 for next in [
9053 FrameDamage::None,
9054 FrameDamage::Full,
9055 FrameDamage::Rects(vec![a]),
9056 ] {
9057 let mut acc = FrameDamage::Full;
9058 FrameReport::merge_into(&mut acc, &next);
9059 assert_eq!(acc, FrameDamage::Full, "Full is absorbing");
9060 }
9061 for start in [FrameDamage::None, FrameDamage::Rects(vec![a])] {
9063 let mut acc = start;
9064 FrameReport::merge_into(&mut acc, &FrameDamage::Full);
9065 assert_eq!(acc, FrameDamage::Full);
9066 }
9067
9068 let mut acc = FrameDamage::None;
9070 FrameReport::merge_into(&mut acc, &FrameDamage::Rects(vec![a]));
9071 assert_eq!(acc, FrameDamage::Rects(vec![a]));
9072
9073 FrameReport::merge_into(&mut acc, &FrameDamage::Rects(vec![b, a]));
9075 assert_eq!(acc, FrameDamage::Rects(vec![a, b, a]));
9076 }
9077
9078 #[test]
9079 fn frame_report_merge_into_keeps_empty_rects_distinct_from_none() {
9080 let mut acc = FrameDamage::Rects(Vec::new());
9083 FrameReport::merge_into(&mut acc, &FrameDamage::Rects(Vec::new()));
9084 assert_eq!(acc, FrameDamage::Rects(Vec::new()));
9085 assert_eq!(acc.rect_count(), 0);
9086 assert!(!acc.is_none());
9087 let mut acc = FrameDamage::None;
9089 FrameReport::merge_into(&mut acc, &FrameDamage::Rects(Vec::new()));
9090 assert_eq!(acc, FrameDamage::Rects(Vec::new()));
9091 }
9092
9093 #[test]
9094 fn frame_report_record_frame_keeps_last_frame_and_accumulated_damage_apart() {
9095 let a = rect(0.0, 0.0, 4.0, 4.0);
9096 let mut r = synced_report();
9097
9098 record(&mut r, FrameDamage::Rects(vec![a]), FrameDamage::Full);
9099 assert_eq!(r.frame_index, 1);
9100 assert_eq!(r.frames_since_reset, 1);
9101 assert_eq!(r.paint_damage, FrameDamage::Rects(vec![a]));
9102 assert_eq!(r.present_damage, FrameDamage::Full);
9103 assert_eq!(r.accumulated_paint_damage, FrameDamage::Rects(vec![a]));
9104 assert_eq!(r.accumulated_present_damage, FrameDamage::Full);
9105
9106 record(&mut r, FrameDamage::None, FrameDamage::None);
9109 assert_eq!(r.frame_index, 2);
9110 assert_eq!(r.frames_since_reset, 2);
9111 assert_eq!(r.paint_damage, FrameDamage::None);
9112 assert_eq!(r.present_damage, FrameDamage::None);
9113 assert_eq!(r.accumulated_paint_damage, FrameDamage::Rects(vec![a]));
9114 assert_eq!(r.accumulated_present_damage, FrameDamage::Full);
9115 }
9116
9117 #[test]
9118 fn frame_report_accumulated_rects_grow_without_dedup() {
9119 let a = rect(0.0, 0.0, 1.0, 1.0);
9120 let mut r = synced_report();
9121 for _ in 0..5 {
9122 record(&mut r, FrameDamage::Rects(vec![a]), FrameDamage::None);
9123 }
9124 assert_eq!(r.accumulated_paint_damage.rect_count(), 5);
9128 assert_eq!(r.frames_since_reset, 5);
9129 }
9130
9131 #[test]
9132 fn frame_report_counters_saturate_and_wrap_as_documented() {
9133 let mut r = synced_report();
9134 r.frames_since_reset = u32::MAX;
9135 r.frame_index = u64::MAX;
9136
9137 record(&mut r, FrameDamage::None, FrameDamage::None);
9138
9139 assert_eq!(r.frames_since_reset, u32::MAX);
9141 assert_eq!(r.frame_index, 0);
9143 }
9144
9145 #[test]
9146 fn frame_report_reset_counters_clears_only_the_sticky_fields() {
9147 let mut r = FrameReport {
9148 frame_index: 42,
9149 terminal_result: 7,
9150 paint_damage: FrameDamage::Full,
9151 present_damage: FrameDamage::Full,
9152 relayout_iterations: 9,
9153 dom_regenerations: 4,
9154 hit_depth_cap: true,
9155 frames_since_reset: 11,
9156 accumulated_paint_damage: FrameDamage::Full,
9157 accumulated_present_damage: FrameDamage::Rects(vec![rect(0.0, 0.0, 1.0, 1.0)]),
9158 ..Default::default()
9159 };
9160
9161 r.reset_counters();
9162
9163 assert_eq!(r.relayout_iterations, 0);
9164 assert_eq!(r.dom_regenerations, 0);
9165 assert!(!r.hit_depth_cap);
9166 assert_eq!(r.frames_since_reset, 0);
9167 assert_eq!(r.accumulated_paint_damage, FrameDamage::None);
9168 assert_eq!(r.accumulated_present_damage, FrameDamage::None);
9169 assert_eq!(r.frame_index, 42);
9171 assert_eq!(r.terminal_result, 7);
9172 assert_eq!(r.paint_damage, FrameDamage::Full);
9173 assert_eq!(r.present_damage, FrameDamage::Full);
9174
9175 r.reset_counters();
9177 assert_eq!(r.frames_since_reset, 0);
9178 }
9179
9180 #[test]
9181 fn frame_report_sync_generation_resets_once_per_request() {
9182 let mut r = synced_report();
9183 r.relayout_iterations = 7;
9184 r.dom_regenerations = 3;
9185 r.hit_depth_cap = true;
9186 r.frames_since_reset = 5;
9187 r.accumulated_paint_damage = FrameDamage::Full;
9188 r.frame_index = 42;
9189 r.paint_damage = FrameDamage::Full;
9190
9191 r.sync_generation_to(0);
9193 assert_eq!(r.relayout_iterations, 7);
9194 assert_eq!(r.accumulated_paint_damage, FrameDamage::Full);
9195
9196 r.sync_generation_to(1);
9197 assert_eq!(r.relayout_iterations, 0);
9198 assert_eq!(r.dom_regenerations, 0);
9199 assert!(!r.hit_depth_cap);
9200 assert_eq!(r.frames_since_reset, 0);
9201 assert_eq!(r.accumulated_paint_damage, FrameDamage::None);
9202 assert_eq!(r.accumulated_present_damage, FrameDamage::None);
9203 assert_eq!(r.frame_index, 42);
9205 assert_eq!(r.paint_damage, FrameDamage::Full);
9206 assert_eq!(r.reset_generation, 1);
9207
9208 r.relayout_iterations = 3;
9210 r.sync_generation_to(1);
9211 assert_eq!(r.relayout_iterations, 3, "sync must fire once per request");
9212 }
9213
9214 #[test]
9219 fn frame_report_as_of_generation_applies_a_pending_reset_to_readers() {
9220 let mut r = synced_report();
9221 r.relayout_iterations = 4;
9222 r.dom_regenerations = 2;
9223 r.frames_since_reset = 9;
9224 r.accumulated_paint_damage = FrameDamage::Full;
9225 r.paint_damage = FrameDamage::Full;
9226
9227 let same = r.as_of_generation(0);
9229 assert_eq!(same, r);
9230
9231 let after = r.as_of_generation(1);
9234 assert_eq!(after.relayout_iterations, 0);
9235 assert_eq!(after.dom_regenerations, 0);
9236 assert_eq!(after.frames_since_reset, 0);
9237 assert_eq!(after.accumulated_paint_damage, FrameDamage::None);
9238 assert_eq!(after.paint_damage, FrameDamage::Full);
9240 assert_eq!(r.relayout_iterations, 4);
9241 }
9242
9243 #[test]
9247 fn frame_report_reset_request_is_per_window() {
9248 let mut a = fresh_window();
9249 let mut b = fresh_window();
9250 a.frame_report.relayout_iterations = 3;
9251 b.frame_report.relayout_iterations = 3;
9252
9253 a.request_frame_report_reset();
9254
9255 assert_eq!(a.frame_report_synced().relayout_iterations, 0);
9256 assert_eq!(
9257 b.frame_report_synced().relayout_iterations,
9258 3,
9259 "a reset on window A leaked into window B"
9260 );
9261
9262 a.sync_frame_report();
9263 b.sync_frame_report();
9264 assert_eq!(a.frame_report.relayout_iterations, 0);
9265 assert_eq!(b.frame_report.relayout_iterations, 3);
9266 }
9267
9268 #[test]
9273 fn duration_to_millis_system_boundaries() {
9274 assert_eq!(duration_to_millis(sys_dur_ms(0)), 0);
9275 assert_eq!(duration_to_millis(sys_dur_ms(1)), 1);
9276 assert_eq!(duration_to_millis(sys_dur_ms(999)), 999);
9277 assert_eq!(duration_to_millis(sys_dur_ms(1_000)), 1_000);
9278 assert_eq!(duration_to_millis(sys_dur_ms(1_500)), 1_500);
9279 assert_eq!(
9281 duration_to_millis(Duration::System(SystemTimeDiff {
9282 secs: 0,
9283 nanos: 999_999
9284 })),
9285 0
9286 );
9287 assert_eq!(
9288 duration_to_millis(Duration::System(SystemTimeDiff {
9289 secs: 0,
9290 nanos: 1_000_000
9291 })),
9292 1
9293 );
9294 }
9295
9296 #[test]
9297 fn duration_to_millis_at_the_top_of_the_u64_range() {
9298 assert_eq!(duration_to_millis(sys_dur_ms(u64::MAX)), u64::MAX);
9301 let max_normalised = Duration::System(SystemTimeDiff {
9304 secs: u64::MAX,
9305 nanos: 999_999_999,
9306 });
9307 let ms = duration_to_millis(max_normalised);
9308 assert_eq!(ms, duration_to_millis(max_normalised), "deterministic");
9309 assert_eq!(ms, u64::MAX);
9313 }
9314
9315 #[test]
9316 fn duration_to_millis_tick_boundaries() {
9317 assert_eq!(duration_to_millis(tick_dur(0)), 0);
9324 assert_eq!(duration_to_millis(tick_dur(1)), 16);
9325 assert_eq!(duration_to_millis(tick_dur(u64::MAX)), u64::MAX);
9326 }
9327
9328 #[test]
9329 fn default_durations_are_the_advertised_500ms_and_200ms() {
9330 assert_eq!(default_duration_500ms(), sys_dur_ms(500));
9331 assert_eq!(default_duration_200ms(), sys_dur_ms(200));
9332 assert_eq!(duration_to_millis(default_duration_500ms()), 500);
9333 assert_eq!(duration_to_millis(default_duration_200ms()), 200);
9334 assert_ne!(default_duration_500ms(), default_duration_200ms());
9335 assert!(default_duration_500ms().greater_than(&default_duration_200ms()));
9336 }
9337
9338 #[test]
9343 fn edge_distance_for_a_rect_fully_inside_its_container() {
9344 let d = calculate_edge_distance(rect(10.0, 10.0, 20.0, 20.0), rect(0.0, 0.0, 100.0, 100.0));
9345 assert_eq!(d.left, 10.0);
9346 assert_eq!(d.right, 70.0);
9347 assert_eq!(d.top, 10.0);
9348 assert_eq!(d.bottom, 70.0);
9349 }
9350
9351 #[test]
9352 fn edge_distance_clamps_negative_overhang_to_zero() {
9353 let d = calculate_edge_distance(rect(-50.0, -50.0, 10.0, 10.0), rect(0.0, 0.0, 100.0, 100.0));
9355 assert_eq!(d.left, 0.0);
9356 assert_eq!(d.top, 0.0);
9357 assert_eq!(d.right, 140.0);
9358 assert_eq!(d.bottom, 140.0);
9359 let d = calculate_edge_distance(rect(0.0, 0.0, 1000.0, 1000.0), rect(0.0, 0.0, 100.0, 100.0));
9361 assert_eq!(d.left, 0.0);
9362 assert_eq!(d.top, 0.0);
9363 assert_eq!(d.right, 0.0);
9364 assert_eq!(d.bottom, 0.0);
9365 }
9366
9367 #[test]
9368 fn edge_distance_never_returns_nan_or_a_negative_number() {
9369 let hostile = [
9370 (rect(f32::NAN, f32::NAN, f32::NAN, f32::NAN), rect(0.0, 0.0, 100.0, 100.0)),
9371 (rect(0.0, 0.0, 10.0, 10.0), rect(f32::NAN, f32::NAN, f32::NAN, f32::NAN)),
9372 (
9373 rect(f32::INFINITY, f32::INFINITY, 1.0, 1.0),
9374 rect(0.0, 0.0, 100.0, 100.0),
9375 ),
9376 (
9377 rect(0.0, 0.0, f32::INFINITY, f32::INFINITY),
9378 rect(0.0, 0.0, f32::INFINITY, f32::INFINITY),
9379 ),
9380 (LogicalRect::zero(), LogicalRect::zero()),
9381 (
9382 rect(f32::MIN, f32::MIN, f32::MAX, f32::MAX),
9383 rect(f32::MAX, f32::MAX, f32::MIN, f32::MIN),
9384 ),
9385 ];
9386 for (r, c) in hostile {
9387 let d = calculate_edge_distance(r, c);
9388 for (name, v) in [
9389 ("left", d.left),
9390 ("right", d.right),
9391 ("top", d.top),
9392 ("bottom", d.bottom),
9393 ] {
9394 assert!(!v.is_nan(), "{name} is NaN for rect={r:?} container={c:?}");
9395 assert!(v >= 0.0, "{name} is negative ({v}) for rect={r:?}");
9396 }
9397 }
9398 let d = calculate_edge_distance(
9400 rect(f32::NAN, f32::NAN, 1.0, 1.0),
9401 rect(0.0, 0.0, 100.0, 100.0),
9402 );
9403 assert_eq!(d.left, 0.0);
9404 assert_eq!(d.top, 0.0);
9405 }
9406
9407 #[test]
9412 fn instant_scroll_delta_is_zero_when_bounds_sit_comfortably_inside() {
9413 let d = calculate_instant_scroll_delta(rect(20.0, 20.0, 10.0, 10.0), rect(0.0, 0.0, 100.0, 100.0));
9414 assert_eq!(d, pos(0.0, 0.0));
9415 }
9416
9417 #[test]
9418 fn instant_scroll_delta_pushes_by_the_five_px_padding() {
9419 let d = calculate_instant_scroll_delta(rect(0.0, 0.0, 10.0, 10.0), rect(0.0, 0.0, 100.0, 100.0));
9421 assert_eq!(d, pos(-5.0, -5.0));
9422 let d = calculate_instant_scroll_delta(rect(3.0, 3.0, 1.0, 1.0), rect(0.0, 0.0, 100.0, 100.0));
9424 assert_eq!(d, pos(-2.0, -2.0));
9425 let d = calculate_instant_scroll_delta(rect(95.0, 95.0, 10.0, 10.0), rect(0.0, 0.0, 100.0, 100.0));
9427 assert_eq!(d, pos(10.0, 10.0));
9428 }
9429
9430 #[test]
9431 fn instant_scroll_delta_near_edge_branch_wins_for_an_oversized_rect() {
9432 let d = calculate_instant_scroll_delta(rect(0.0, 0.0, 500.0, 500.0), rect(0.0, 0.0, 100.0, 100.0));
9435 assert_eq!(d, pos(-5.0, -5.0));
9436 }
9437
9438 #[test]
9439 fn instant_scroll_delta_with_nan_bounds_is_zero_not_nan() {
9440 let d = calculate_instant_scroll_delta(
9441 rect(f32::NAN, f32::NAN, f32::NAN, f32::NAN),
9442 rect(0.0, 0.0, 100.0, 100.0),
9443 );
9444 assert!(!d.x.is_nan() && !d.y.is_nan(), "NaN must not leak into the scroll delta");
9445 assert_eq!(d, pos(0.0, 0.0));
9446 }
9447
9448 #[test]
9449 fn instant_scroll_delta_saturates_instead_of_panicking_on_huge_bounds() {
9450 let d = calculate_instant_scroll_delta(
9451 rect(f32::MAX, f32::MAX, f32::MAX, f32::MAX),
9452 rect(0.0, 0.0, 100.0, 100.0),
9453 );
9454 assert!(d.x.is_infinite() && d.x > 0.0);
9455 assert!(d.y.is_infinite() && d.y > 0.0);
9456 let d = calculate_instant_scroll_delta(LogicalRect::zero(), LogicalRect::zero());
9458 assert_eq!(d, pos(-5.0, -5.0));
9459 }
9460
9461 fn edges(left: f32, right: f32, top: f32, bottom: f32) -> EdgeDistance {
9466 EdgeDistance {
9467 left,
9468 right,
9469 top,
9470 bottom,
9471 }
9472 }
9473
9474 #[test]
9475 fn accelerated_scroll_delta_dead_zone_produces_no_movement() {
9476 assert_eq!(
9477 calculate_accelerated_scroll_delta(edges(0.0, 0.0, 0.0, 0.0)),
9478 pos(0.0, 0.0)
9479 );
9480 assert_eq!(
9482 calculate_accelerated_scroll_delta(edges(19.999, 1000.0, 19.999, 1000.0)),
9483 pos(0.0, 0.0)
9484 );
9485 }
9486
9487 #[test]
9488 fn accelerated_scroll_delta_zone_boundaries_are_exact() {
9489 let cases = [
9492 (19.999_f32, 0.0_f32),
9493 (20.0, -2.0),
9494 (49.999, -2.0),
9495 (50.0, -4.0),
9496 (99.999, -4.0),
9497 (100.0, -8.0),
9498 (199.999, -8.0),
9499 (200.0, -16.0),
9500 (1e9, -16.0),
9501 ];
9502 for (dist, expected_x) in cases {
9503 let d = calculate_accelerated_scroll_delta(edges(dist, f32::MAX, dist, f32::MAX));
9504 assert_eq!(d.x, expected_x, "left={dist}");
9505 assert_eq!(d.y, expected_x, "top={dist}");
9506 }
9507 }
9508
9509 #[test]
9510 fn accelerated_scroll_delta_picks_the_nearer_edge_and_signs_it() {
9511 let d = calculate_accelerated_scroll_delta(edges(30.0, 1000.0, 30.0, 1000.0));
9513 assert_eq!(d, pos(-2.0, -2.0));
9514 let d = calculate_accelerated_scroll_delta(edges(1000.0, 60.0, 1000.0, 60.0));
9516 assert_eq!(d, pos(4.0, 4.0));
9517 let d = calculate_accelerated_scroll_delta(edges(60.0, 60.0, 60.0, 60.0));
9519 assert_eq!(d, pos(4.0, 4.0));
9520 }
9521
9522 #[test]
9523 fn accelerated_scroll_delta_treats_negative_distances_as_dead_zone() {
9524 let d = calculate_accelerated_scroll_delta(edges(-100.0, 1000.0, -100.0, 1000.0));
9525 assert_eq!(d.x, 0.0);
9526 assert_eq!(d.y, 0.0);
9527 }
9528
9529 #[test]
9530 fn accelerated_scroll_delta_with_nan_distances_falls_into_the_fastest_zone() {
9531 let d = calculate_accelerated_scroll_delta(edges(f32::NAN, f32::NAN, f32::NAN, f32::NAN));
9535 assert_eq!(d, pos(16.0, 16.0));
9536 assert!(!d.x.is_nan() && !d.y.is_nan());
9537 }
9538
9539 #[test]
9540 fn accelerated_scroll_delta_speed_is_always_bounded() {
9541 let vals = [
9542 0.0_f32,
9543 -1.0,
9544 19.9,
9545 20.0,
9546 50.0,
9547 100.0,
9548 200.0,
9549 f32::MAX,
9550 f32::INFINITY,
9551 f32::NEG_INFINITY,
9552 f32::NAN,
9553 ];
9554 for l in vals {
9555 for r in vals {
9556 let d = calculate_accelerated_scroll_delta(edges(l, r, l, r));
9557 assert!(d.x.abs() <= 16.0, "|x| out of range for ({l}, {r}): {}", d.x);
9558 assert!(d.y.abs() <= 16.0, "|y| out of range for ({l}, {r}): {}", d.y);
9559 assert!(!d.x.is_nan() && !d.y.is_nan());
9560 }
9561 }
9562 }
9563
9564 fn opacity(last: Option<Instant>, now: Instant, delay: Duration, dur: Duration) -> f32 {
9569 LayoutWindow::calculate_scrollbar_opacity(last, now, delay, dur)
9570 }
9571
9572 #[test]
9573 fn scrollbar_opacity_without_activity_is_fully_transparent() {
9574 assert_eq!(
9575 opacity(None, tick(1_000), tick_dur(500), tick_dur(200)),
9576 0.0
9577 );
9578 }
9579
9580 #[test]
9581 fn scrollbar_opacity_stays_opaque_through_the_delay_window() {
9582 for elapsed in [0_u64, 1, 250, 499, 500] {
9583 let v = opacity(
9584 Some(tick(0)),
9585 tick(elapsed),
9586 tick_dur(500),
9587 tick_dur(200),
9588 );
9589 assert_eq!(v, 1.0, "must stay opaque at elapsed={elapsed}");
9590 }
9591 }
9592
9593 #[test]
9594 fn scrollbar_opacity_fades_linearly_then_pins_at_zero() {
9595 let v = opacity(Some(tick(0)), tick(600), tick_dur(500), tick_dur(200));
9597 assert!((v - 0.5).abs() < 1e-4, "expected ~0.5, got {v}");
9598 let v = opacity(Some(tick(0)), tick(700), tick_dur(500), tick_dur(200));
9600 assert!(v.abs() < 1e-4, "expected ~0.0, got {v}");
9601 assert_eq!(
9603 opacity(Some(tick(0)), tick(1_000_000), tick_dur(500), tick_dur(200)),
9604 0.0
9605 );
9606 }
9607
9608 #[test]
9609 fn scrollbar_opacity_handles_a_clock_that_went_backwards() {
9610 let v = opacity(Some(tick(500)), tick(0), tick_dur(500), tick_dur(200));
9613 assert_eq!(v, 1.0);
9614 }
9615
9616 #[test]
9617 fn scrollbar_opacity_survives_zero_length_delay_and_fade() {
9618 for (delay, dur) in [
9621 (tick_dur(0), tick_dur(200)),
9622 (tick_dur(500), tick_dur(0)),
9623 (tick_dur(0), tick_dur(0)),
9624 ] {
9625 for now in [tick(0), tick(1), tick(500), tick(u64::MAX)] {
9626 let v = opacity(Some(tick(0)), now, delay, dur);
9627 assert!(!v.is_nan(), "NaN opacity for delay={delay:?} dur={dur:?}");
9628 assert!((0.0..=1.0).contains(&v), "opacity {v} out of range");
9629 }
9630 }
9631 }
9632
9633 #[test]
9641 fn scrollbar_opacity_fades_when_a_tick_clock_drives_wall_clock_constants() {
9642 let v = opacity(Some(tick(0)), tick(600), sys_dur_ms(500), sys_dur_ms(200));
9643 assert_eq!(v, 0.0, "10s of frames is long past a 500ms + 200ms fade");
9644
9645 assert_eq!(opacity(Some(tick(0)), tick(29), sys_dur_ms(500), sys_dur_ms(200)), 1.0);
9648 let v = opacity(Some(tick(0)), tick(36), sys_dur_ms(500), sys_dur_ms(200));
9650 assert!((v - 0.5).abs() < 0.05, "expected ~0.5 halfway through the fade, got {v}");
9651 }
9652
9653 #[test]
9654 fn scrollbar_opacity_is_always_a_valid_alpha() {
9655 let times = [0_u64, 1, 499, 500, 501, 700, 10_000, u64::MAX];
9656 let durs = [
9657 tick_dur(0),
9658 tick_dur(1),
9659 tick_dur(200),
9660 tick_dur(500),
9661 tick_dur(u64::MAX),
9662 ];
9663 for now in times {
9664 for delay in durs {
9665 for dur in durs {
9666 let v = opacity(Some(tick(0)), tick(now), delay, dur);
9667 assert!(!v.is_nan(), "NaN at now={now} delay={delay:?} dur={dur:?}");
9668 assert!(
9669 (0.0..=1.0).contains(&v),
9670 "opacity {v} out of range at now={now} delay={delay:?} dur={dur:?}"
9671 );
9672 }
9673 }
9674 }
9675 }
9676
9677 #[test]
9682 fn remap_scrollbar_hit_id_rewrites_every_variant_of_the_target_dom() {
9683 let dom = DomId { inner: 3 };
9684 let map = crate::managers::NodeIdMap::from_pairs([(NodeId::new(5), NodeId::new(9))]);
9685 let ctors: [fn(DomId, NodeId) -> ScrollbarHitId; 4] = [
9686 ScrollbarHitId::VerticalTrack,
9687 ScrollbarHitId::VerticalThumb,
9688 ScrollbarHitId::HorizontalTrack,
9689 ScrollbarHitId::HorizontalThumb,
9690 ];
9691 for ctor in ctors {
9692 assert_eq!(
9693 remap_scrollbar_hit_id(ctor(dom, NodeId::new(5)), dom, &map),
9694 Some(ctor(dom, NodeId::new(9)))
9695 );
9696 }
9697 }
9698
9699 #[test]
9700 fn remap_scrollbar_hit_id_drops_state_for_unmounted_nodes() {
9701 let dom = DomId { inner: 3 };
9702 let map = crate::managers::NodeIdMap::from_pairs([(NodeId::new(5), NodeId::new(9))]);
9703 assert_eq!(
9705 remap_scrollbar_hit_id(ScrollbarHitId::VerticalThumb(dom, NodeId::new(7)), dom, &map),
9706 None
9707 );
9708 let empty = crate::managers::NodeIdMap::from_pairs(Vec::<(NodeId, NodeId)>::new());
9710 assert_eq!(
9711 remap_scrollbar_hit_id(ScrollbarHitId::VerticalThumb(dom, NodeId::new(5)), dom, &empty),
9712 None
9713 );
9714 assert_eq!(
9716 remap_scrollbar_hit_id(
9717 ScrollbarHitId::HorizontalTrack(dom, NodeId::new(usize::MAX)),
9718 dom,
9719 &map
9720 ),
9721 None
9722 );
9723 }
9724
9725 #[test]
9726 fn remap_scrollbar_hit_id_passes_other_doms_through_untouched() {
9727 let dom = DomId { inner: 3 };
9728 let other = DomId { inner: 4 };
9729 let map = crate::managers::NodeIdMap::from_pairs([(NodeId::new(5), NodeId::new(9))]);
9730 let id = ScrollbarHitId::VerticalThumb(other, NodeId::new(5));
9732 assert_eq!(remap_scrollbar_hit_id(id, dom, &map), Some(id));
9733 let id = ScrollbarHitId::HorizontalThumb(other, NodeId::new(usize::MAX));
9734 assert_eq!(remap_scrollbar_hit_id(id, dom, &map), Some(id));
9735 }
9736
9737 #[test]
9742 fn layout_result_new_stores_its_arguments_verbatim() {
9743 let lr = LayoutResult::new(DisplayList::default(), Vec::new());
9744 assert!(lr.warnings.is_empty());
9745 assert!(lr.display_list.items.is_empty());
9746
9747 let lr = LayoutResult::new(
9748 DisplayList::default(),
9749 vec![String::new(), "a".repeat(10_000), "☃/🇺🇳/\u{202e}".to_string()],
9750 );
9751 assert_eq!(lr.warnings.len(), 3);
9752 assert_eq!(lr.warnings[1].len(), 10_000);
9753 assert_eq!(lr.warnings[2].chars().count(), 6);
9755 }
9756
9757 #[test]
9762 fn layout_window_new_starts_completely_empty() {
9763 let w = fresh_window();
9764 assert_eq!(w.get_timer_ids().len(), 0);
9765 assert_eq!(w.get_thread_ids().len(), 0);
9766 assert_eq!(w.get_dom_ids().len(), 0);
9767 assert!(w.layout_results.is_empty());
9768 assert!(w.timers.is_empty());
9769 assert!(w.threads.is_empty());
9770 assert_eq!(w.frame_report, FrameReport::default());
9771 assert!(w.layout_cache.tree.is_none());
9772 assert!(w.layout_cache.calculated_positions.is_empty());
9773 assert!(w.layout_cache.viewport.is_none());
9774 assert!(w.currently_dragging_thumb.is_none());
9775 assert!(w.scan_used_fonts().is_empty());
9776 assert!(w.scan_used_images(&ImageCache::default()).is_empty());
9777 assert!(!w.skip_gpu_sync);
9778 }
9779
9780 #[test]
9781 fn layout_window_constructors_hand_out_unique_document_and_namespace_ids() {
9782 let a = fresh_window();
9783 let b = fresh_window();
9784 assert_ne!(a.document_id, b.document_id);
9785 assert_ne!(a.id_namespace, b.id_namespace);
9786 assert_ne!(a.document_id.namespace_id, a.id_namespace);
9787 }
9788
9789 #[test]
9790 fn layout_window_new_with_shared_fonts_accepts_an_empty_shared_map() {
9791 let shared: Arc<std::sync::Mutex<HashMap<rust_fontconfig::FontId, FontRef>>> =
9792 Arc::new(std::sync::Mutex::new(HashMap::new()));
9793 let a = LayoutWindow::new_with_shared_fonts(FcFontCache::default(), Arc::clone(&shared))
9794 .expect("shared-font constructor must succeed on an empty map");
9795 let b = LayoutWindow::new_with_shared_fonts(FcFontCache::default(), shared)
9796 .expect("shared-font constructor must succeed twice");
9797 assert!(a.layout_results.is_empty());
9798 assert!(b.layout_results.is_empty());
9799 assert_ne!(a.id_namespace, b.id_namespace);
9800 }
9801
9802 #[cfg(feature = "pdf")]
9803 #[test]
9804 fn layout_window_new_paged_accepts_degenerate_page_sizes() {
9805 for page in [
9806 LogicalSize::zero(),
9807 size(-1.0, -1.0),
9808 size(f32::MAX, f32::MAX),
9809 size(f32::NAN, f32::NAN),
9810 size(f32::INFINITY, f32::INFINITY),
9811 ] {
9812 let w = LayoutWindow::new_paged(FcFontCache::default(), page)
9813 .expect("new_paged must not fail on a degenerate page size");
9814 assert!(w.layout_results.is_empty());
9815 }
9816 }
9817
9818 #[test]
9823 fn node_getters_return_none_on_an_empty_window_for_every_hostile_id() {
9824 let w = fresh_window();
9825 for dom in [DomId::ROOT_ID, DomId { inner: 0 }, DomId { inner: usize::MAX }] {
9826 for node in hostile_node_ids() {
9827 let id = DomNodeId { dom, node };
9828 assert!(w.get_node_size(id).is_none(), "size {id:?}");
9829 assert!(w.get_node_position(id).is_none(), "position {id:?}");
9830 assert!(w.get_node_hit_test_bounds(id).is_none(), "bounds {id:?}");
9831 assert!(w.get_parent(id).is_none(), "parent {id:?}");
9832 assert!(w.get_first_child(id).is_none(), "first_child {id:?}");
9833 assert!(w.get_last_child(id).is_none(), "last_child {id:?}");
9834 assert!(w.get_next_sibling(id).is_none(), "next_sibling {id:?}");
9835 assert!(w.get_previous_sibling(id).is_none(), "prev_sibling {id:?}");
9836 }
9837 }
9838 }
9839
9840 #[test]
9841 fn node_getters_survive_hostile_ids_against_a_real_styled_dom() {
9842 let w = window_with_fixture();
9843 for node in hostile_node_ids() {
9846 let id = DomNodeId {
9847 dom: DomId::ROOT_ID,
9848 node,
9849 };
9850 assert!(w.get_parent(id).is_none(), "parent {id:?}");
9851 assert!(w.get_first_child(id).is_none(), "first_child {id:?}");
9852 assert!(w.get_last_child(id).is_none(), "last_child {id:?}");
9853 assert!(w.get_next_sibling(id).is_none(), "next_sibling {id:?}");
9854 assert!(w.get_previous_sibling(id).is_none(), "prev_sibling {id:?}");
9855 assert!(w.get_node_size(id).is_none(), "size {id:?}");
9856 assert!(w.get_node_position(id).is_none(), "position {id:?}");
9857 assert!(w.get_node_hit_test_bounds(id).is_none(), "bounds {id:?}");
9858 }
9859 let wrong_dom = DomNodeId {
9861 dom: DomId { inner: 77 },
9862 node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(0))),
9863 };
9864 assert!(wrong_dom.node.into_crate_internal().is_some());
9865 assert!(w.get_parent(wrong_dom).is_none());
9866 assert!(w.get_first_child(wrong_dom).is_none());
9867 }
9868
9869 #[test]
9870 fn hierarchy_getters_agree_with_each_other_on_a_real_dom() {
9871 let w = window_with_fixture();
9872 let root = dnid(0);
9873 assert_eq!(
9874 w.get_layout_result(&DomId::ROOT_ID)
9875 .expect("fixture must be registered")
9876 .styled_dom
9877 .node_hierarchy
9878 .len(),
9879 4,
9880 "fixture is body + 3 divs"
9881 );
9882
9883 assert!(w.get_parent(root).is_none());
9885 let first = w.get_first_child(root).expect("root must have a first child");
9886 let last = w.get_last_child(root).expect("root must have a last child");
9887 assert_ne!(first, last);
9888 assert_eq!(w.get_parent(first), Some(root));
9889 assert_eq!(w.get_parent(last), Some(root));
9890
9891 let mid = w.get_next_sibling(first).expect("second child");
9893 assert_eq!(w.get_next_sibling(mid), Some(last));
9894 assert_eq!(w.get_next_sibling(last), None, "last child has no successor");
9895
9896 assert_eq!(w.get_previous_sibling(last), Some(mid));
9898 assert_eq!(w.get_previous_sibling(mid), Some(first));
9899 assert_eq!(w.get_previous_sibling(first), None);
9900
9901 for leaf in [first, mid, last] {
9903 assert!(w.get_first_child(leaf).is_none(), "{leaf:?} must be a leaf");
9904 assert!(w.get_last_child(leaf).is_none(), "{leaf:?} must be a leaf");
9905 }
9906 }
9907
9908 #[test]
9909 fn geometry_getters_return_none_without_a_layout_pass() {
9910 let w = window_with_fixture();
9913 for i in 0..4 {
9914 let id = dnid(i);
9915 assert!(w.get_node_size(id).is_none(), "size for node {i}");
9916 assert!(w.get_node_position(id).is_none(), "position for node {i}");
9917 assert!(w.get_node_hit_test_bounds(id).is_none(), "bounds for node {i}");
9918 }
9919 }
9920
9921 #[test]
9922 fn scan_used_resources_is_empty_for_an_empty_display_list() {
9923 let w = window_with_fixture();
9924 assert!(w.scan_used_fonts().is_empty());
9925 assert!(w.scan_used_images(&ImageCache::default()).is_empty());
9926 let mut w = w;
9928 w.layout_results
9929 .insert(DomId { inner: 1 }, bare_layout_result(fixture_dom()));
9930 w.layout_results
9931 .insert(DomId { inner: 2 }, bare_layout_result(fixture_dom()));
9932 assert_eq!(w.get_dom_ids().len(), 3);
9933 assert!(w.scan_used_fonts().is_empty());
9934 assert!(w.scan_used_images(&ImageCache::default()).is_empty());
9935 }
9936
9937 #[test]
9942 fn node_has_text_content_sees_direct_and_child_text() {
9943 let plain = fixture_dom();
9944 for i in 0..4 {
9945 assert!(
9946 !LayoutWindow::node_has_text_content(&plain, NodeId::new(i)),
9947 "node {i} of a text-free DOM must not report text"
9948 );
9949 }
9950
9951 let with_text =
9953 StyledDom::create_from_dom(Dom::create_body().with_child(Dom::create_text("hello")));
9954 assert_eq!(with_text.node_hierarchy.len(), 2);
9955 assert!(
9956 LayoutWindow::node_has_text_content(&with_text, NodeId::new(0)),
9957 "the parent of a text node has text content"
9958 );
9959 assert!(
9960 LayoutWindow::node_has_text_content(&with_text, NodeId::new(1)),
9961 "a text node is itself text content"
9962 );
9963
9964 for s in ["", "🇺🇳👩👩👧👦", "\u{202e}\u{0}"] {
9966 let d = StyledDom::create_from_dom(Dom::create_body().with_child(Dom::create_text(s)));
9967 assert!(LayoutWindow::node_has_text_content(&d, NodeId::new(1)), "{s:?}");
9968 assert!(LayoutWindow::node_has_text_content(&d, NodeId::new(0)), "{s:?}");
9969 }
9970 }
9971
9972 #[test]
9973 fn is_text_selectable_honours_user_select_none() {
9974 let dom = StyledDom::create_from_dom(
9975 Dom::create_body()
9976 .with_child(Dom::create_div())
9977 .with_child(Dom::create_div().with_css("user-select: none;")),
9978 );
9979 assert_eq!(dom.node_hierarchy.len(), 3);
9980 assert!(
9981 LayoutWindow::is_text_selectable(&dom, NodeId::new(1)),
9982 "default is selectable"
9983 );
9984 assert!(
9985 !LayoutWindow::is_text_selectable(&dom, NodeId::new(2)),
9986 "user-select: none must opt out"
9987 );
9988 }
9989
9990 #[test]
9991 fn contenteditable_lookups_are_false_for_an_unknown_dom() {
9992 let w = window_with_fixture();
9993 let missing = DomId { inner: 999 };
9994 assert!(!w.is_node_contenteditable_internal(missing, NodeId::new(usize::MAX)));
9997 assert!(!w.is_node_contenteditable_inherited_internal(missing, NodeId::new(usize::MAX)));
9998 for i in 0..4 {
10000 assert!(!w.is_node_contenteditable_internal(DomId::ROOT_ID, NodeId::new(i)));
10001 }
10002 }
10003
10004 #[test]
10005 fn contenteditable_is_detected_and_inherited() {
10006 let mut w = fresh_window();
10007 let dom = StyledDom::create_from_dom(
10008 Dom::create_body()
10009 .with_child(Dom::create_div().with_contenteditable(true).with_child(Dom::create_div())),
10010 );
10011 assert_eq!(dom.node_hierarchy.len(), 3);
10012 w.layout_results
10013 .insert(DomId::ROOT_ID, bare_layout_result(dom));
10014
10015 assert!(!w.is_node_contenteditable_internal(DomId::ROOT_ID, NodeId::new(0)));
10016 assert!(w.is_node_contenteditable_internal(DomId::ROOT_ID, NodeId::new(1)));
10017 assert!(!w.is_node_contenteditable_internal(DomId::ROOT_ID, NodeId::new(2)));
10019 assert!(w.is_node_contenteditable_inherited_internal(DomId::ROOT_ID, NodeId::new(2)));
10021 }
10022
10023 extern "C" fn time_tick_0() -> Instant {
10028 Instant::Tick(azul_core::task::SystemTick { tick_counter: 0 })
10029 }
10030
10031 extern "C" fn time_tick_1000() -> Instant {
10032 Instant::Tick(azul_core::task::SystemTick { tick_counter: 1_000 })
10033 }
10034
10035 extern "C" fn time_tick_max() -> Instant {
10036 Instant::Tick(azul_core::task::SystemTick {
10037 tick_counter: u64::MAX,
10038 })
10039 }
10040
10041 extern "C" fn time_system_now() -> Instant {
10042 Instant::now()
10043 }
10044
10045 #[test]
10046 fn timer_add_get_remove_round_trips_and_overwrites_by_id() {
10047 let mut w = fresh_window();
10048 let id = TimerId { id: 1 };
10049 assert!(w.get_timer(&id).is_none());
10050 assert!(w.remove_timer(&id).is_none(), "removing an absent timer is None");
10051
10052 w.add_timer(id, Timer::default());
10053 assert!(w.get_timer(&id).is_some());
10054 assert!(w.get_timer_mut(&id).is_some());
10055 assert_eq!(w.get_timer_ids().len(), 1);
10056
10057 w.add_timer(id, Timer::default());
10059 assert_eq!(w.get_timer_ids().len(), 1);
10060
10061 assert!(w.remove_timer(&id).is_some());
10062 assert!(w.get_timer(&id).is_none());
10063 assert_eq!(w.get_timer_ids().len(), 0);
10064 assert!(w.remove_timer(&id).is_none());
10066 }
10067
10068 #[test]
10069 fn timer_ids_survive_extreme_id_values() {
10070 let mut w = fresh_window();
10071 let lo = TimerId { id: 0 };
10072 let hi = TimerId { id: usize::MAX };
10073 w.add_timer(lo, Timer::default());
10074 w.add_timer(hi, Timer::default());
10075 assert_eq!(w.get_timer_ids().len(), 2);
10076 assert!(w.get_timer(&lo).is_some());
10077 assert!(w.get_timer(&hi).is_some());
10078 assert!(w.remove_timer(&hi).is_some());
10079 assert_eq!(w.get_timer_ids().len(), 1);
10080 }
10081
10082 #[test]
10083 fn tick_timers_reports_every_registered_timer_regardless_of_the_clock() {
10084 let mut w = fresh_window();
10085 assert!(w.tick_timers(tick(0)).is_empty(), "no timers => nothing ready");
10086
10087 for i in 0..3 {
10088 w.add_timer(TimerId { id: i }, Timer::default());
10089 }
10090 for now in [tick(0), tick(u64::MAX), Instant::now()] {
10093 let ready = w.tick_timers(now);
10094 assert_eq!(ready.len(), 3);
10095 }
10096 }
10097
10098 #[test]
10099 fn time_until_next_timer_ms_is_none_without_timers() {
10100 let w = fresh_window();
10101 let cb = azul_core::task::GetSystemTimeCallback { cb: time_tick_0 };
10102 assert_eq!(
10103 w.time_until_next_timer_ms(&cb),
10104 None,
10105 "no timers => the caller may block indefinitely"
10106 );
10107 }
10108
10109 #[test]
10110 fn time_until_next_timer_ms_reports_zero_for_an_overdue_timer() {
10111 let mut w = fresh_window();
10112 w.add_timer(TimerId { id: 1 }, Timer::default());
10115 let clocks: [azul_core::task::GetSystemTimeCallbackType; 3] =
10116 [time_tick_0, time_tick_1000, time_tick_max];
10117 for cb in clocks {
10118 let cb = azul_core::task::GetSystemTimeCallback { cb };
10119 assert_eq!(w.time_until_next_timer_ms(&cb), Some(0));
10120 }
10121 }
10122
10123 #[test]
10124 fn time_until_next_timer_ms_takes_the_minimum_across_timers() {
10125 let mut w = fresh_window();
10126 w.add_timer(
10127 TimerId { id: 1 },
10128 Timer::default().with_interval(tick_dur(5_000)),
10129 );
10130 w.add_timer(
10131 TimerId { id: 2 },
10132 Timer::default().with_interval(tick_dur(2_000)),
10133 );
10134 w.add_timer(
10135 TimerId { id: 3 },
10136 Timer::default().with_interval(tick_dur(9_000)),
10137 );
10138 let cb = azul_core::task::GetSystemTimeCallback { cb: time_tick_0 };
10143 assert_eq!(w.time_until_next_timer_ms(&cb), Some(33_333));
10144
10145 let cb = azul_core::task::GetSystemTimeCallback { cb: time_tick_1000 };
10147 assert_eq!(w.time_until_next_timer_ms(&cb), Some(16_666));
10148
10149 let cb = azul_core::task::GetSystemTimeCallback { cb: time_tick_max };
10151 assert_eq!(w.time_until_next_timer_ms(&cb), Some(0));
10152 }
10153
10154 #[test]
10155 fn time_until_next_timer_ms_does_not_overflow_on_a_max_interval() {
10156 let mut w = fresh_window();
10157 w.add_timer(
10158 TimerId { id: 1 },
10159 Timer::default().with_interval(tick_dur(u64::MAX)),
10160 );
10161 let cb = azul_core::task::GetSystemTimeCallback { cb: time_tick_0 };
10162 assert_eq!(w.time_until_next_timer_ms(&cb), Some(u64::MAX));
10163 }
10164
10165 #[test]
10166 fn time_until_next_timer_ms_saturates_across_mismatched_clock_kinds() {
10167 let mut w = fresh_window();
10168 w.add_timer(
10175 TimerId { id: 1 },
10176 Timer::default().with_interval(tick_dur(5_000)),
10177 );
10178 let cb = azul_core::task::GetSystemTimeCallback {
10179 cb: time_system_now,
10180 };
10181 assert_eq!(w.time_until_next_timer_ms(&cb), Some(0));
10182 }
10183
10184 #[test]
10185 fn create_tooltip_delay_timer_encodes_the_hover_time_as_a_one_shot_delay() {
10186 let w = fresh_window();
10187 for ms in [0_u32, 1, 500, u32::MAX] {
10188 let t = w.create_tooltip_delay_timer(ms);
10189 assert_eq!(
10190 t.delay,
10191 azul_core::task::OptionDuration::Some(sys_dur_ms(u64::from(ms))),
10192 "hover_time_ms={ms}"
10193 );
10194 assert_eq!(
10195 t.interval,
10196 azul_core::task::OptionDuration::None,
10197 "the tooltip timer is one-shot"
10198 );
10199 assert_eq!(t.timeout, azul_core::task::OptionDuration::None);
10200 assert_eq!(t.run_count, 0);
10201 assert!(matches!(t.last_run, azul_core::task::OptionInstant::None));
10202 match &t.delay {
10204 azul_core::task::OptionDuration::Some(d) => {
10205 assert_eq!(duration_to_millis(*d), u64::from(ms));
10206 }
10207 azul_core::task::OptionDuration::None => panic!("delay must be set"),
10208 }
10209 }
10210 }
10211
10212 #[test]
10213 fn create_cursor_blink_timer_is_a_repeating_530ms_timer() {
10214 let w = fresh_window();
10215 let t = w.create_cursor_blink_timer(&FullWindowState::default());
10216 assert_eq!(
10217 t.delay,
10218 azul_core::task::OptionDuration::None,
10219 "the blink timer starts immediately"
10220 );
10221 assert_eq!(t.timeout, azul_core::task::OptionDuration::None);
10222 assert_eq!(t.run_count, 0);
10223 match &t.interval {
10224 azul_core::task::OptionDuration::Some(d) => assert_eq!(
10225 duration_to_millis(*d),
10226 crate::managers::text_edit::CURSOR_BLINK_INTERVAL_MS
10227 ),
10228 azul_core::task::OptionDuration::None => panic!("interval must be set"),
10229 }
10230 }
10231
10232 #[test]
10237 fn create_cursor_blink_timer_carries_a_tick_interval_through_as_ticks() {
10238 let mut w = fresh_window();
10239 w.text_edit_manager
10240 .blink
10241 .set_blink_interval(Duration::from_ticks(5));
10242
10243 let t = w.create_cursor_blink_timer(&FullWindowState::default());
10244 assert_eq!(
10245 t.interval,
10246 azul_core::task::OptionDuration::Some(Duration::from_ticks(5)),
10247 "the tick unit must survive into the timer"
10248 );
10249 assert_eq!(t.tick_millis(), 83);
10252 }
10253
10254 #[test]
10257 fn duration_to_millis_converts_ticks_at_the_nominal_frame_rate() {
10258 assert_eq!(duration_to_millis(Duration::from_ticks(60)), 1_000);
10259 assert_eq!(duration_to_millis(Duration::from_ticks(5)), 83);
10260 assert_eq!(duration_to_millis(Duration::from_ticks(1)), 16);
10261 assert_eq!(duration_to_millis(Duration::from_ticks(0)), 0);
10262 assert_eq!(duration_to_millis(Duration::from_millis(530)), 530);
10264 assert_eq!(duration_to_millis(Duration::from_millis(0)), 0);
10265 }
10266
10267 #[test]
10272 fn thread_getters_are_none_on_an_empty_window() {
10273 let mut w = fresh_window();
10274 let id = ThreadId::unique();
10275 assert_eq!(w.get_thread_ids().len(), 0);
10276 assert!(w.get_thread(&id).is_none());
10277 assert!(w.get_thread_mut(&id).is_none());
10278 assert!(w.remove_thread(&id).is_none());
10279 assert!(w.get_thread(&ThreadId::unique()).is_none());
10281 assert_eq!(w.get_thread_ids().len(), 0);
10282 }
10283
10284 #[test]
10289 fn get_scroll_position_is_none_before_anything_is_set() {
10290 let w = fresh_window();
10291 assert_eq!(w.get_scroll_position(DomId::ROOT_ID, NodeId::new(0)), None);
10292 assert_eq!(
10293 w.get_scroll_position(DomId { inner: usize::MAX }, NodeId::new(usize::MAX)),
10294 None
10295 );
10296 }
10297
10298 #[test]
10299 fn set_then_get_scroll_position_round_trips_inside_the_scrollable_range() {
10300 let mut w = fresh_window();
10301 let node = NodeId::new(4);
10302 let scroll = ScrollPosition {
10303 parent_rect: rect(0.0, 0.0, 100.0, 100.0),
10304 children_rect: rect(0.0, 0.0, 200.0, 200.0),
10305 };
10306 w.set_scroll_position(DomId::ROOT_ID, node, scroll);
10307 assert_eq!(w.get_scroll_position(DomId::ROOT_ID, node), Some(scroll));
10308
10309 let scrolled = ScrollPosition {
10311 parent_rect: rect(0.0, 0.0, 100.0, 100.0),
10312 children_rect: rect(50.0, 25.0, 200.0, 200.0),
10313 };
10314 w.set_scroll_position(DomId::ROOT_ID, node, scrolled);
10315 assert_eq!(w.get_scroll_position(DomId::ROOT_ID, node), Some(scrolled));
10316
10317 assert_eq!(w.get_scroll_position(DomId::ROOT_ID, NodeId::new(5)), None);
10319 assert_eq!(w.get_scroll_position(DomId { inner: 1 }, node), None);
10320 }
10321
10322 #[test]
10323 fn set_scroll_position_clamps_out_of_range_and_nan_offsets() {
10324 let mut w = fresh_window();
10325 let node = NodeId::new(0);
10326 let container = rect(0.0, 0.0, 100.0, 100.0);
10327 for (requested, expected) in [
10329 (pos(99_999.0, 99_999.0), pos(100.0, 100.0)),
10330 (pos(-500.0, -500.0), pos(0.0, 0.0)),
10331 (pos(f32::INFINITY, f32::NEG_INFINITY), pos(100.0, 0.0)),
10332 (pos(f32::NAN, f32::NAN), pos(0.0, 0.0)),
10333 ] {
10334 w.set_scroll_position(
10335 DomId::ROOT_ID,
10336 node,
10337 ScrollPosition {
10338 parent_rect: container,
10339 children_rect: LogicalRect::new(requested, size(200.0, 200.0)),
10340 },
10341 );
10342 let got = w
10343 .get_scroll_position(DomId::ROOT_ID, node)
10344 .expect("state was just written");
10345 assert!(
10346 !got.children_rect.origin.x.is_nan() && !got.children_rect.origin.y.is_nan(),
10347 "clamped scroll offset must never be NaN (requested {requested:?})"
10348 );
10349 assert_eq!(
10350 got.children_rect.origin, expected,
10351 "requested {requested:?} must clamp to {expected:?}"
10352 );
10353 }
10354 }
10355
10356 #[test]
10357 fn set_scroll_position_on_a_non_scrollable_node_pins_to_the_origin() {
10358 let mut w = fresh_window();
10359 let node = NodeId::new(0);
10360 w.set_scroll_position(
10362 DomId::ROOT_ID,
10363 node,
10364 ScrollPosition {
10365 parent_rect: rect(0.0, 0.0, 100.0, 100.0),
10366 children_rect: rect(40.0, 40.0, 10.0, 10.0),
10367 },
10368 );
10369 let got = w.get_scroll_position(DomId::ROOT_ID, node).expect("written");
10370 assert_eq!(got.children_rect.origin, pos(0.0, 0.0));
10371 }
10372
10373 #[test]
10374 fn get_nested_scroll_states_always_contains_the_requested_dom_key() {
10375 let w = fresh_window();
10376 let nested = w.get_nested_scroll_states(DomId::ROOT_ID);
10380 assert_eq!(nested.len(), 1);
10381 assert!(nested[&DomId::ROOT_ID].is_empty());
10382
10383 let mut w = w;
10384 let node = NodeId::new(2);
10385 w.set_scroll_position(
10386 DomId::ROOT_ID,
10387 node,
10388 ScrollPosition {
10389 parent_rect: rect(0.0, 0.0, 100.0, 100.0),
10390 children_rect: rect(0.0, 0.0, 200.0, 200.0),
10391 },
10392 );
10393 let nested = w.get_nested_scroll_states(DomId::ROOT_ID);
10394 let inner = &nested[&DomId::ROOT_ID];
10395 assert_eq!(inner.len(), 1);
10396 assert!(inner.contains_key(&NodeHierarchyItemId::from_crate_internal(Some(node))));
10397 assert!(w.get_nested_scroll_states(DomId { inner: 9 })[&DomId { inner: 9 }].is_empty());
10399 }
10400
10401 #[test]
10406 fn selection_accessors_are_the_documented_no_ops() {
10407 let mut w = fresh_window();
10408 let state = SelectionState {
10409 selections: Vec::<Selection>::new().into(),
10410 node_id: DomNodeId {
10411 dom: DomId::ROOT_ID,
10412 node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(0))),
10413 },
10414 };
10415 assert!(w.get_selection(DomId::ROOT_ID).is_none());
10416 w.set_selection(DomId::ROOT_ID, state.clone());
10417 assert!(w.get_selection(DomId::ROOT_ID).is_none());
10419 assert!(w.get_selection(DomId { inner: usize::MAX }).is_none());
10420 w.set_selection(DomId { inner: 42 }, state);
10421 assert!(w.get_selection(DomId { inner: 42 }).is_none());
10422 }
10423
10424 #[test]
10429 fn clear_caches_drops_every_layout_result_and_is_idempotent() {
10430 let mut w = window_with_fixture();
10431 w.layout_results
10432 .insert(DomId { inner: 1 }, bare_layout_result(fixture_dom()));
10433 w.set_scroll_position(
10434 DomId::ROOT_ID,
10435 NodeId::new(0),
10436 ScrollPosition {
10437 parent_rect: rect(0.0, 0.0, 100.0, 100.0),
10438 children_rect: rect(0.0, 0.0, 200.0, 200.0),
10439 },
10440 );
10441 assert_eq!(w.get_dom_ids().len(), 2);
10442 assert!(w.get_scroll_position(DomId::ROOT_ID, NodeId::new(0)).is_some());
10443
10444 w.clear_caches();
10445
10446 assert_eq!(w.get_dom_ids().len(), 0);
10447 assert!(w.layout_results.is_empty());
10448 assert!(w.layout_cache.tree.is_none());
10449 assert!(w.layout_cache.calculated_positions.is_empty());
10450 assert!(w.layout_cache.viewport.is_none());
10451 assert!(w.layout_cache.cached_display_list.is_none());
10452 assert_eq!(w.layout_cache.prev_dom_ptr, 0);
10453 assert!(
10454 w.get_scroll_position(DomId::ROOT_ID, NodeId::new(0)).is_none(),
10455 "clear_caches replaces the ScrollManager"
10456 );
10457
10458 w.clear_caches();
10460 assert_eq!(w.get_dom_ids().len(), 0);
10461 fresh_window().clear_caches();
10462 }
10463
10464 #[test]
10469 fn gpu_cache_accessors_are_keyed_per_dom_and_created_on_demand() {
10470 let mut w = fresh_window();
10471 let a = DomId { inner: 0 };
10472 let b = DomId { inner: usize::MAX };
10473 assert!(w.get_gpu_cache(&a).is_none());
10474 assert!(w.get_gpu_cache_mut(&a).is_none());
10475
10476 assert!(w.get_or_create_gpu_cache(a).transform_keys.is_empty());
10477 assert!(w.get_gpu_cache(&a).is_some());
10478 assert!(w.get_gpu_cache(&b).is_none(), "creation must not leak across DOMs");
10479
10480 assert!(w.get_or_create_gpu_cache(a).transform_keys.is_empty());
10482 assert!(w.get_or_create_gpu_cache(b).transform_keys.is_empty());
10483 assert!(w.get_gpu_cache(&b).is_some());
10484 }
10485
10486 #[test]
10487 fn layout_result_accessors_track_get_dom_ids() {
10488 let mut w = fresh_window();
10489 assert!(w.get_layout_result(&DomId::ROOT_ID).is_none());
10490 assert!(w.get_layout_result_mut(&DomId::ROOT_ID).is_none());
10491 assert_eq!(w.get_dom_ids().len(), 0);
10492
10493 w.layout_results
10494 .insert(DomId::ROOT_ID, bare_layout_result(fixture_dom()));
10495 assert!(w.get_layout_result(&DomId::ROOT_ID).is_some());
10496 assert!(w.get_layout_result_mut(&DomId::ROOT_ID).is_some());
10497 assert_eq!(w.get_dom_ids().len(), 1);
10498 assert!(w.get_layout_result(&DomId { inner: 1 }).is_none());
10499 }
10500
10501 fn laid_out(styled_dom: StyledDom, w: f32, h: f32) -> LayoutWindow {
10510 let mut win = fresh_window();
10511 let mut ws = FullWindowState::default();
10512 ws.size.dimensions = size(w, h);
10513 let rr = RendererResources::default();
10514 let sc = ExternalSystemCallbacks::rust_internal();
10515 let mut dbg = None;
10516 win.layout_and_generate_display_list(styled_dom, &ws, &rr, &sc, &mut dbg)
10517 .expect("layout must succeed on a well-formed DOM");
10518 win
10519 }
10520
10521 #[test]
10522 fn layout_and_generate_display_list_populates_results_for_a_plain_dom() {
10523 let win = laid_out(fixture_dom(), 200.0, 150.0);
10524 let lr = win
10525 .get_layout_result(&DomId::ROOT_ID)
10526 .expect("root layout result must exist after a successful layout");
10527 assert!(
10528 !lr.layout_tree.nodes.is_empty(),
10529 "the layout tree must carry nodes"
10530 );
10531 let root_bounds = win.get_node_bounds(DomId::ROOT_ID, NodeId::new(0));
10533 assert!(root_bounds.is_some(), "root node must have bounds");
10534 }
10535
10536 #[test]
10537 fn get_node_bounds_is_none_for_missing_dom_and_missing_node() {
10538 let win = laid_out(fixture_dom(), 200.0, 150.0);
10539 assert!(win.get_node_bounds(DomId { inner: 9 }, NodeId::new(0)).is_none());
10541 assert!(win
10543 .get_node_bounds(DomId::ROOT_ID, NodeId::new(9_999))
10544 .is_none());
10545 }
10546
10547 #[test]
10548 fn scroll_position_roundtrips_and_is_none_when_unregistered() {
10549 let mut win = fresh_window();
10550 let dom = DomId::ROOT_ID;
10551 let node = NodeId::new(1);
10552 assert!(
10553 win.get_scroll_position(dom, node).is_none(),
10554 "an unregistered node has no scroll position"
10555 );
10556 let sp = ScrollPosition {
10557 parent_rect: rect(0.0, 0.0, 100.0, 100.0),
10558 children_rect: rect(0.0, 0.0, 100.0, 500.0),
10559 };
10560 win.set_scroll_position(dom, node, sp);
10561 assert!(
10562 win.get_scroll_position(dom, node).is_some(),
10563 "after set_scroll_position the node must have a position"
10564 );
10565 }
10566
10567 #[test]
10568 fn timer_scheduling_helpers_handle_the_empty_case() {
10569 let mut win = fresh_window();
10570 let sc = ExternalSystemCallbacks::rust_internal();
10571 assert!(
10572 win.time_until_next_timer_ms(&sc.get_system_time_fn).is_none(),
10573 "no timers => can block indefinitely"
10574 );
10575 assert!(
10576 win.tick_timers(tick(0)).is_empty(),
10577 "no timers => nothing is ready"
10578 );
10579 }
10580
10581 #[test]
10582 fn scroll_node_into_view_on_an_empty_window_yields_no_adjustments() {
10583 let mut win = fresh_window();
10584 let sc = ExternalSystemCallbacks::rust_internal();
10585 let now = (sc.get_system_time_fn.cb)();
10586 let adjustments = win.scroll_node_into_view(
10587 dnid(0),
10588 crate::managers::scroll_into_view::ScrollIntoViewOptions::start(),
10589 now,
10590 );
10591 assert!(
10592 adjustments.is_empty(),
10593 "a node with no scrollable ancestor cannot be scrolled into view"
10594 );
10595 }
10596
10597 #[test]
10598 fn find_scrollable_ancestor_is_none_without_a_layout_tree() {
10599 let win = fresh_window();
10600 assert!(
10601 win.find_scrollable_ancestor(dnid(0)).is_none(),
10602 "no layout_cache tree => no scrollable ancestor"
10603 );
10604 }
10605
10606 #[test]
10607 fn clear_caches_drops_layout_results_and_scroll_state() {
10608 let mut win = laid_out(fixture_dom(), 200.0, 150.0);
10609 win.set_scroll_position(
10610 DomId::ROOT_ID,
10611 NodeId::new(1),
10612 ScrollPosition {
10613 parent_rect: rect(0.0, 0.0, 100.0, 100.0),
10614 children_rect: rect(0.0, 0.0, 100.0, 500.0),
10615 },
10616 );
10617 assert!(!win.layout_results.is_empty());
10618 assert!(win.get_scroll_position(DomId::ROOT_ID, NodeId::new(1)).is_some());
10619
10620 win.clear_caches();
10621
10622 assert!(win.layout_results.is_empty(), "layout results must be cleared");
10623 assert!(
10624 win.get_scroll_position(DomId::ROOT_ID, NodeId::new(1)).is_none(),
10625 "the ScrollManager must be reset"
10626 );
10627 assert!(win.layout_cache.tree.is_none());
10628 }
10629
10630 #[test]
10631 fn finalize_pending_focus_changes_is_false_without_a_pending_request() {
10632 let mut win = fresh_window();
10633 assert!(
10634 !win.finalize_pending_focus_changes(),
10635 "nothing pending => no cursor initialization happened"
10636 );
10637 }
10638
10639 #[test]
10640 fn text_node_navigation_is_none_on_an_empty_window() {
10641 let win = fresh_window();
10642 assert!(win.find_next_text_node(&DomId::ROOT_ID, NodeId::new(0)).is_none());
10643 assert!(win.find_prev_text_node(&DomId::ROOT_ID, NodeId::new(0)).is_none());
10644 }
10645
10646 #[test]
10647 fn text_node_navigation_walks_a_real_dom_without_running_off_the_end() {
10648 let dom = StyledDom::create_from_dom(
10649 Dom::create_body().with_child(Dom::create_text("hello world")),
10650 );
10651 let win = laid_out(dom, 200.0, 150.0);
10652 let node_count = win
10653 .get_layout_result(&DomId::ROOT_ID)
10654 .unwrap()
10655 .styled_dom
10656 .node_hierarchy
10657 .len();
10658 let last = NodeId::new(node_count.saturating_sub(1));
10660 assert!(
10661 win.find_next_text_node(&DomId::ROOT_ID, last).is_none(),
10662 "no node exists after the last one"
10663 );
10664 assert!(
10666 win.find_prev_text_node(&DomId::ROOT_ID, NodeId::new(0)).is_none(),
10667 "no node exists before the first one"
10668 );
10669 }
10670
10671 #[test]
10672 fn resize_window_relayouts_and_returns_a_display_list() {
10673 let mut win = laid_out(fixture_dom(), 200.0, 150.0);
10674 let rr = RendererResources::default();
10675 let sc = ExternalSystemCallbacks::rust_internal();
10676 let mut dbg = None;
10677 let dl = win
10680 .resize_window(fixture_dom(), size(400.0, 300.0), &rr, &sc, &mut dbg)
10681 .expect("resize_window must succeed");
10682 let _ = dl;
10684 assert!(
10685 win.get_layout_result(&DomId::ROOT_ID).is_some(),
10686 "resize must leave a laid-out DOM behind"
10687 );
10688 }
10689}