1use std::{
12 collections::{BTreeMap, BTreeSet},
13 sync::{
14 atomic::{AtomicUsize, Ordering},
15 Arc,
16 },
17};
18
19use azul_core::{
20 animation::UpdateImageType,
21 callbacks::{FocusTarget, HidpiAdjustedBounds, IFrameCallbackReason, Update},
22 dom::{
23 AccessibilityAction, AttributeType, Dom, DomId, DomIdVec, DomNodeId, NodeId, NodeType, On,
24 },
25 events::{EasingFunction, EventFilter, FocusEventFilter, HoverEventFilter},
26 geom::{LogicalPosition, LogicalRect, LogicalSize, OptionLogicalPosition},
27 gl::OptionGlContextPtr,
28 gpu::{GpuScrollbarOpacityEvent, GpuValueCache},
29 hit_test::{DocumentId, ScrollPosition, ScrollbarHitId},
30 refany::{OptionRefAny, RefAny},
31 resources::{
32 Epoch, FontKey, GlTextureCache, IdNamespace, ImageCache, ImageMask, ImageRef, ImageRefHash,
33 OpacityKey, RendererResources,
34 },
35 selection::{
36 CursorAffinity, GraphemeClusterId, Selection, SelectionAnchor, SelectionFocus,
37 SelectionRange, SelectionState, TextCursor, TextSelection,
38 },
39 styled_dom::{
40 collect_nodes_in_document_order, is_before_in_document_order, NodeHierarchyItemId,
41 StyledDom,
42 },
43 task::{
44 Duration, Instant, SystemTickDiff, SystemTimeDiff, TerminateTimer, ThreadId, ThreadIdVec,
45 ThreadSendMsg, TimerId, TimerIdVec,
46 },
47 window::{CursorPosition, RawWindowHandle, RendererType},
48 FastBTreeSet, FastHashMap,
49};
50use azul_css::{
51 css::Css,
52 props::{
53 basic::FontRef,
54 property::{CssProperty, CssPropertyVec},
55 },
56 AzString, LayoutDebugMessage, OptionString,
57};
58use rust_fontconfig::FcFontCache;
59
60#[cfg(feature = "icu")]
61use crate::icu::IcuLocalizerHandle;
62use crate::{
63 callbacks::{
64 CallCallbacksResult, Callback, ExternalSystemCallbacks, FocusUpdateRequest, MenuCallback,
65 },
66 managers::{
67 gpu_state::GpuStateManager,
68 iframe::IFrameManager,
69 scroll_state::{ScrollManager, ScrollStates},
70 },
71 solver3::{
72 self, cache::LayoutCache as Solver3LayoutCache, display_list::DisplayList,
73 layout_tree::LayoutTree,
74 },
75 text3::{
76 cache::{
77 FontManager, FontSelector, FontStyle, InlineContent, LayoutCache as TextLayoutCache,
78 LayoutError, ShapedItem, StyleProperties, StyledRun, TextBoundary, UnifiedConstraints,
79 UnifiedLayout,
80 },
81 default::PathLoader,
82 },
83 thread::{OptionThreadReceiveMsg, Thread, ThreadReceiveMsg, ThreadWriteBackMsg},
84 timer::Timer,
85 window_state::{FullWindowState, WindowCreateOptions},
86};
87
88static DOCUMENT_ID_COUNTER: AtomicUsize = AtomicUsize::new(0);
90static ID_NAMESPACE_COUNTER: AtomicUsize = AtomicUsize::new(0);
91
92fn new_document_id() -> DocumentId {
94 let namespace_id = new_id_namespace();
95 let id = DOCUMENT_ID_COUNTER.fetch_add(1, Ordering::Relaxed) as u32;
96 DocumentId { namespace_id, id }
97}
98
99#[derive(Debug, Copy, Clone, PartialEq, Eq)]
101pub enum CursorNavigationDirection {
102 Up,
104 Down,
106 Left,
108 Right,
110 LineStart,
112 LineEnd,
114 DocumentStart,
116 DocumentEnd,
118}
119
120#[derive(Debug, Clone)]
122pub enum CursorMovementResult {
123 MovedWithinNode(TextCursor),
125 MovedToNode {
127 dom_id: DomId,
128 node_id: NodeId,
129 cursor: TextCursor,
130 },
131 AtBoundary {
133 boundary: TextBoundary,
134 cursor: TextCursor,
135 },
136}
137
138#[derive(Debug, Clone)]
140pub struct NoCursorDestination {
141 pub reason: String,
142}
143
144#[derive(Debug, Clone)]
149pub enum CursorBlinkTimerAction {
150 Start(crate::timer::Timer),
152 Stop,
154 NoChange,
156}
157
158fn new_id_namespace() -> IdNamespace {
160 let id = ID_NAMESPACE_COUNTER.fetch_add(1, Ordering::Relaxed) as u32;
161 IdNamespace(id)
162}
163
164extern "C" fn cursor_blink_timer_destructor(_: RefAny) {
170 }
172
173pub extern "C" fn cursor_blink_timer_callback(
183 _data: RefAny,
184 mut info: crate::timer::TimerCallbackInfo,
185) -> azul_core::callbacks::TimerCallbackReturn {
186 use azul_core::callbacks::{TimerCallbackReturn, Update};
187 use azul_core::task::TerminateTimer;
188
189 let now = info.get_current_time();
191
192 info.set_cursor_visibility_toggle();
210
211 TimerCallbackReturn {
213 should_update: Update::RefreshDom,
214 should_terminate: TerminateTimer::Continue,
215 }
216}
217
218#[derive(Debug)]
220pub struct DomLayoutResult {
221 pub styled_dom: StyledDom,
223 pub layout_tree: LayoutTree,
225 pub calculated_positions: BTreeMap<usize, LogicalPosition>,
227 pub viewport: LogicalRect,
229 pub display_list: DisplayList,
231 pub scroll_ids: BTreeMap<usize, u64>,
234 pub scroll_id_to_node_id: BTreeMap<u64, NodeId>,
237}
238
239#[derive(Debug, Clone)]
241pub struct ScrollbarDragState {
242 pub hit_id: ScrollbarHitId,
243 pub initial_mouse_pos: LogicalPosition,
244 pub initial_scroll_offset: LogicalPosition,
245}
246
247pub use crate::managers::text_input::PendingTextEdit;
251
252#[derive(Debug, Clone)]
255pub struct TextConstraintsCache {
256 pub constraints: BTreeMap<(DomId, NodeId), UnifiedConstraints>,
258}
259
260impl Default for TextConstraintsCache {
261 fn default() -> Self {
262 Self {
263 constraints: BTreeMap::new(),
264 }
265 }
266}
267
268#[derive(Debug, Clone)]
271pub struct DirtyTextNode {
272 pub content: Vec<InlineContent>,
274 pub cursor: Option<TextCursor>,
276 pub needs_ancestor_relayout: bool,
278}
279
280#[derive(Debug, Default)]
285pub struct CallbackChangeResult {
286 pub timers: FastHashMap<TimerId, crate::timer::Timer>,
288 pub threads: FastHashMap<ThreadId, crate::thread::Thread>,
290 pub timers_removed: FastBTreeSet<TimerId>,
292 pub threads_removed: FastBTreeSet<ThreadId>,
294 pub windows_created: Vec<crate::window_state::WindowCreateOptions>,
296 pub menus_to_open: Vec<(azul_core::menu::Menu, Option<LogicalPosition>)>,
298 pub tooltips_to_show: Vec<(AzString, LogicalPosition)>,
300 pub hide_tooltip: bool,
302 pub stop_propagation: bool,
304 pub prevent_default: bool,
306 pub focus_target: Option<FocusTarget>,
308 pub words_changed: BTreeMap<DomId, BTreeMap<NodeId, AzString>>,
310 pub images_changed: BTreeMap<DomId, BTreeMap<NodeId, (ImageRef, UpdateImageType)>>,
312 pub image_callbacks_changed: BTreeMap<DomId, FastBTreeSet<NodeId>>,
315 pub iframes_to_update: BTreeMap<DomId, FastBTreeSet<NodeId>>,
318 pub image_masks_changed: BTreeMap<DomId, BTreeMap<NodeId, ImageMask>>,
320 pub css_properties_changed: BTreeMap<DomId, BTreeMap<NodeId, CssPropertyVec>>,
322 pub nodes_scrolled: BTreeMap<DomId, BTreeMap<NodeHierarchyItemId, LogicalPosition>>,
324 pub modified_window_state: FullWindowState,
326 pub queued_window_states: Vec<FullWindowState>,
329 pub hit_test_update_requested: Option<LogicalPosition>,
332 pub text_input_triggered: Vec<(azul_core::dom::DomNodeId, Vec<azul_core::events::EventFilter>)>,
335}
336
337pub struct LayoutWindow {
346 #[cfg(feature = "pdf")]
348 pub fragmentation_context: crate::paged::FragmentationContext,
349 pub layout_cache: Solver3LayoutCache,
351 pub text_cache: TextLayoutCache,
353 pub font_manager: FontManager<FontRef>,
355 pub image_cache: ImageCache,
357 pub layout_results: BTreeMap<DomId, DomLayoutResult>,
359 pub scroll_manager: ScrollManager,
361 pub gesture_drag_manager: crate::managers::gesture::GestureAndDragManager,
363 pub focus_manager: crate::managers::focus_cursor::FocusManager,
365 pub cursor_manager: crate::managers::cursor::CursorManager,
367 pub file_drop_manager: crate::managers::file_drop::FileDropManager,
369 pub selection_manager: crate::managers::selection::SelectionManager,
371 pub clipboard_manager: crate::managers::clipboard::ClipboardManager,
373 pub drag_drop_manager: crate::managers::drag_drop::DragDropManager,
375 pub hover_manager: crate::managers::hover::HoverManager,
377 pub iframe_manager: IFrameManager,
379 pub gpu_state_manager: GpuStateManager,
381 pub a11y_manager: crate::managers::a11y::A11yManager,
383 pub timers: BTreeMap<TimerId, Timer>,
385 pub threads: BTreeMap<ThreadId, Thread>,
387 pub renderer_resources: RendererResources,
389 pub renderer_type: Option<RendererType>,
391 pub previous_window_state: Option<FullWindowState>,
393 pub current_window_state: FullWindowState,
396 pub document_id: DocumentId,
399 pub id_namespace: IdNamespace,
401 pub epoch: Epoch,
404 pub gl_texture_cache: GlTextureCache,
406 currently_dragging_thumb: Option<ScrollbarDragState>,
408 pub text_input_manager: crate::managers::text_input::TextInputManager,
410 pub undo_redo_manager: crate::managers::undo_redo::UndoRedoManager,
412 text_constraints_cache: TextConstraintsCache,
415 dirty_text_nodes: BTreeMap<(DomId, NodeId), DirtyTextNode>,
419 pub pending_iframe_updates: BTreeMap<DomId, FastBTreeSet<NodeId>>,
422 #[cfg(feature = "icu")]
425 pub icu_localizer: IcuLocalizerHandle,
426}
427
428fn default_duration_500ms() -> Duration {
429 Duration::System(SystemTimeDiff::from_millis(500))
430}
431
432fn default_duration_200ms() -> Duration {
433 Duration::System(SystemTimeDiff::from_millis(200))
434}
435
436fn duration_to_millis(duration: Duration) -> u64 {
441 match duration {
442 #[cfg(feature = "std")]
443 Duration::System(system_diff) => {
444 let std_duration: std::time::Duration = system_diff.into();
445 std_duration.as_millis() as u64
446 }
447 #[cfg(not(feature = "std"))]
448 Duration::System(system_diff) => {
449 system_diff.secs * 1000 + (system_diff.nanos / 1_000_000) as u64
451 }
452 Duration::Tick(tick_diff) => {
453 tick_diff.tick_diff
455 }
456 }
457}
458
459impl LayoutWindow {
460 pub fn new(fc_cache: FcFontCache) -> Result<Self, crate::solver3::LayoutError> {
464 Ok(Self {
465 #[cfg(feature = "pdf")]
467 fragmentation_context: crate::paged::FragmentationContext::new_continuous(800.0),
468 layout_cache: Solver3LayoutCache {
469 tree: None,
470 calculated_positions: BTreeMap::new(),
471 viewport: None,
472 scroll_ids: BTreeMap::new(),
473 scroll_id_to_node_id: BTreeMap::new(),
474 counters: BTreeMap::new(),
475 float_cache: BTreeMap::new(),
476 },
477 text_cache: TextLayoutCache::new(),
478 font_manager: FontManager::new(fc_cache)?,
479 image_cache: ImageCache::default(),
480 layout_results: BTreeMap::new(),
481 scroll_manager: ScrollManager::new(),
482 gesture_drag_manager: crate::managers::gesture::GestureAndDragManager::new(),
483 focus_manager: crate::managers::focus_cursor::FocusManager::new(),
484 cursor_manager: crate::managers::cursor::CursorManager::new(),
485 file_drop_manager: crate::managers::file_drop::FileDropManager::new(),
486 selection_manager: crate::managers::selection::SelectionManager::new(),
487 clipboard_manager: crate::managers::clipboard::ClipboardManager::new(),
488 drag_drop_manager: crate::managers::drag_drop::DragDropManager::new(),
489 hover_manager: crate::managers::hover::HoverManager::new(),
490 iframe_manager: IFrameManager::new(),
491 gpu_state_manager: GpuStateManager::new(
492 default_duration_500ms(),
493 default_duration_200ms(),
494 ),
495 a11y_manager: crate::managers::a11y::A11yManager::new(),
496 timers: BTreeMap::new(),
497 threads: BTreeMap::new(),
498 renderer_resources: RendererResources::default(),
499 renderer_type: None,
500 previous_window_state: None,
501 current_window_state: FullWindowState::default(),
502 document_id: new_document_id(),
503 id_namespace: new_id_namespace(),
504 epoch: Epoch::new(),
505 gl_texture_cache: GlTextureCache::default(),
506 currently_dragging_thumb: None,
507 text_input_manager: crate::managers::text_input::TextInputManager::new(),
508 undo_redo_manager: crate::managers::undo_redo::UndoRedoManager::new(),
509 text_constraints_cache: TextConstraintsCache {
510 constraints: BTreeMap::new(),
511 },
512 dirty_text_nodes: BTreeMap::new(),
513 pending_iframe_updates: BTreeMap::new(),
514 #[cfg(feature = "icu")]
515 icu_localizer: IcuLocalizerHandle::default(),
516 })
517 }
518
519 #[cfg(feature = "pdf")]
532 pub fn new_paged(
533 fc_cache: FcFontCache,
534 page_size: LogicalSize,
535 ) -> Result<Self, crate::solver3::LayoutError> {
536 Ok(Self {
537 fragmentation_context: crate::paged::FragmentationContext::new_paged(page_size),
538 layout_cache: Solver3LayoutCache {
539 tree: None,
540 calculated_positions: BTreeMap::new(),
541 viewport: None,
542 scroll_ids: BTreeMap::new(),
543 scroll_id_to_node_id: BTreeMap::new(),
544 counters: BTreeMap::new(),
545 float_cache: BTreeMap::new(),
546 },
547 text_cache: TextLayoutCache::new(),
548 font_manager: FontManager::new(fc_cache)?,
549 image_cache: ImageCache::default(),
550 layout_results: BTreeMap::new(),
551 scroll_manager: ScrollManager::new(),
552 gesture_drag_manager: crate::managers::gesture::GestureAndDragManager::new(),
553 focus_manager: crate::managers::focus_cursor::FocusManager::new(),
554 cursor_manager: crate::managers::cursor::CursorManager::new(),
555 file_drop_manager: crate::managers::file_drop::FileDropManager::new(),
556 selection_manager: crate::managers::selection::SelectionManager::new(),
557 clipboard_manager: crate::managers::clipboard::ClipboardManager::new(),
558 drag_drop_manager: crate::managers::drag_drop::DragDropManager::new(),
559 hover_manager: crate::managers::hover::HoverManager::new(),
560 iframe_manager: IFrameManager::new(),
561 gpu_state_manager: GpuStateManager::new(
562 default_duration_500ms(),
563 default_duration_200ms(),
564 ),
565 a11y_manager: crate::managers::a11y::A11yManager::new(),
566 timers: BTreeMap::new(),
567 threads: BTreeMap::new(),
568 renderer_resources: RendererResources::default(),
569 renderer_type: None,
570 previous_window_state: None,
571 current_window_state: FullWindowState::default(),
572 document_id: new_document_id(),
573 id_namespace: new_id_namespace(),
574 epoch: Epoch::new(),
575 gl_texture_cache: GlTextureCache::default(),
576 currently_dragging_thumb: None,
577 text_input_manager: crate::managers::text_input::TextInputManager::new(),
578 undo_redo_manager: crate::managers::undo_redo::UndoRedoManager::new(),
579 text_constraints_cache: TextConstraintsCache {
580 constraints: BTreeMap::new(),
581 },
582 dirty_text_nodes: BTreeMap::new(),
583 pending_iframe_updates: BTreeMap::new(),
584 #[cfg(feature = "icu")]
585 icu_localizer: IcuLocalizerHandle::default(),
586 })
587 }
588
589 pub fn layout_and_generate_display_list(
607 &mut self,
608 root_dom: StyledDom,
609 window_state: &FullWindowState,
610 renderer_resources: &RendererResources,
611 system_callbacks: &ExternalSystemCallbacks,
612 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
613 ) -> Result<(), solver3::LayoutError> {
614 self.layout_results.clear();
616
617 if let Some(msgs) = debug_messages.as_mut() {
618 msgs.push(LayoutDebugMessage::info(format!(
619 "[layout_and_generate_display_list] Starting layout for DOM with {} nodes",
620 root_dom.node_data.len()
621 )));
622 }
623
624 let result = self.layout_dom_recursive(
626 root_dom,
627 window_state,
628 renderer_resources,
629 system_callbacks,
630 debug_messages,
631 );
632
633 if let Err(ref e) = result {
634 if let Some(msgs) = debug_messages.as_mut() {
635 msgs.push(LayoutDebugMessage::error(format!(
636 "[layout_and_generate_display_list] Layout FAILED: {:?}",
637 e
638 )));
639 }
640 eprintln!("[layout_and_generate_display_list] Layout FAILED: {:?}", e);
641 } else {
642 if let Some(msgs) = debug_messages.as_mut() {
643 msgs.push(LayoutDebugMessage::info(format!(
644 "[layout_and_generate_display_list] Layout SUCCESS, layout_results count: {}",
645 self.layout_results.len()
646 )));
647 }
648 }
649
650 #[cfg(feature = "a11y")]
653 if result.is_ok() {
654 let a11y_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
656 crate::managers::a11y::A11yManager::update_tree(
657 self.a11y_manager.root_id,
658 &self.layout_results,
659 &self.current_window_state.title,
660 self.current_window_state.size.dimensions,
661 )
662 }));
663
664 match a11y_result {
665 Ok(tree_update) => {
666 self.a11y_manager.last_tree_update = Some(tree_update);
668 }
669 Err(_) => {
670 }
672 }
673 }
674
675 if result.is_ok() {
677 self.scroll_focused_cursor_into_view();
678 }
679
680 result
681 }
682
683 fn layout_dom_recursive(
684 &mut self,
685 mut styled_dom: StyledDom,
686 window_state: &FullWindowState,
687 renderer_resources: &RendererResources,
688 system_callbacks: &ExternalSystemCallbacks,
689 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
690 ) -> Result<(), solver3::LayoutError> {
691 if styled_dom.dom_id.inner == 0 {
692 styled_dom.dom_id = DomId::ROOT_ID;
693 }
694 let dom_id = styled_dom.dom_id;
695
696 let viewport = LogicalRect {
697 origin: LogicalPosition::zero(),
698 size: window_state.size.dimensions,
699 };
700
701 {
704 use crate::{
705 solver3::getters::{
706 collect_and_resolve_font_chains, collect_font_ids_from_chains,
707 compute_fonts_to_load, load_fonts_from_disk, register_embedded_fonts_from_styled_dom,
708 },
709 text3::default::PathLoader,
710 };
711
712 if let Some(msgs) = debug_messages.as_mut() {
713 msgs.push(LayoutDebugMessage::info(
714 "[FontLoading] Starting font resolution for DOM".to_string(),
715 ));
716 }
717
718 register_embedded_fonts_from_styled_dom(&styled_dom, &self.font_manager);
721
722 let chains = collect_and_resolve_font_chains(&styled_dom, &self.font_manager.fc_cache);
724 if let Some(msgs) = debug_messages.as_mut() {
725 msgs.push(LayoutDebugMessage::info(format!(
726 "[FontLoading] Resolved {} font chains",
727 chains.len()
728 )));
729 }
730
731 let required_fonts = collect_font_ids_from_chains(&chains);
733 if let Some(msgs) = debug_messages.as_mut() {
734 msgs.push(LayoutDebugMessage::info(format!(
735 "[FontLoading] Required fonts: {} unique fonts",
736 required_fonts.len()
737 )));
738 }
739
740 let already_loaded = self.font_manager.get_loaded_font_ids();
742 let fonts_to_load = compute_fonts_to_load(&required_fonts, &already_loaded);
743 if let Some(msgs) = debug_messages.as_mut() {
744 msgs.push(LayoutDebugMessage::info(format!(
745 "[FontLoading] Already loaded: {}, need to load: {}",
746 already_loaded.len(),
747 fonts_to_load.len()
748 )));
749 }
750
751 if !fonts_to_load.is_empty() {
753 if let Some(msgs) = debug_messages.as_mut() {
754 msgs.push(LayoutDebugMessage::info(format!(
755 "[FontLoading] Loading {} fonts from disk...",
756 fonts_to_load.len()
757 )));
758 }
759 let loader = PathLoader::new();
760 let load_result = load_fonts_from_disk(
761 &fonts_to_load,
762 &self.font_manager.fc_cache,
763 |bytes, index| loader.load_font(bytes, index),
764 );
765
766 if let Some(msgs) = debug_messages.as_mut() {
767 msgs.push(LayoutDebugMessage::info(format!(
768 "[FontLoading] Loaded {} fonts, {} failed",
769 load_result.loaded.len(),
770 load_result.failed.len()
771 )));
772 }
773
774 self.font_manager.insert_fonts(load_result.loaded);
776
777 for (font_id, error) in &load_result.failed {
779 if let Some(msgs) = debug_messages.as_mut() {
780 msgs.push(LayoutDebugMessage::warning(format!(
781 "[FontLoading] Failed to load font {:?}: {}",
782 font_id, error
783 )));
784 }
785 }
786 }
787
788 self.font_manager.set_font_chain_cache(chains.into_fontconfig_chains());
790 }
791
792 let scroll_offsets = self.scroll_manager.get_scroll_states_for_dom(dom_id);
793 let styled_dom_clone = styled_dom.clone();
794 let gpu_cache = self.gpu_state_manager.get_or_create_cache(dom_id).clone();
795
796 let cursor_is_visible = self.cursor_manager.should_draw_cursor();
798
799 let cursor_location = self.cursor_manager.get_cursor_location().and_then(|loc| {
801 self.cursor_manager.get_cursor().map(|cursor| {
802 (loc.dom_id, loc.node_id, cursor.clone())
803 })
804 });
805
806 let mut display_list = solver3::layout_document(
807 &mut self.layout_cache,
808 &mut self.text_cache,
809 styled_dom,
810 viewport,
811 &self.font_manager,
812 &scroll_offsets,
813 &self.selection_manager.selections,
814 &self.selection_manager.text_selections,
815 debug_messages,
816 Some(&gpu_cache),
817 &self.renderer_resources,
818 self.id_namespace,
819 dom_id,
820 cursor_is_visible,
821 cursor_location,
822 )?;
823
824 let tree = self
825 .layout_cache
826 .tree
827 .clone()
828 .ok_or(solver3::LayoutError::InvalidTree)?;
829
830 let scroll_ids = self.layout_cache.scroll_ids.clone();
832 let scroll_id_to_node_id = self.layout_cache.scroll_id_to_node_id.clone();
833
834 self.gpu_state_manager
836 .update_scrollbar_transforms(dom_id, &self.scroll_manager, &tree);
837
838 let iframes = self.scan_for_iframes(dom_id, &tree, &self.layout_cache.calculated_positions);
840
841 for (node_id, bounds) in iframes {
842 if let Some(child_dom_id) = self.invoke_iframe_callback(
843 dom_id,
844 node_id,
845 bounds,
846 window_state,
847 renderer_resources,
848 system_callbacks,
849 debug_messages,
850 ) {
851 display_list
853 .items
854 .push(crate::solver3::display_list::DisplayListItem::IFrame {
855 child_dom_id,
856 bounds,
857 clip_rect: bounds,
858 });
859 }
860 }
861
862 self.layout_results.insert(
864 dom_id,
865 DomLayoutResult {
866 styled_dom: styled_dom_clone,
867 layout_tree: tree,
868 calculated_positions: self.layout_cache.calculated_positions.clone(),
869 viewport,
870 display_list,
871 scroll_ids,
872 scroll_id_to_node_id,
873 },
874 );
875
876 Ok(())
877 }
878
879 fn scan_for_iframes(
880 &self,
881 dom_id: DomId,
882 layout_tree: &LayoutTree,
883 calculated_positions: &BTreeMap<usize, LogicalPosition>,
884 ) -> Vec<(NodeId, LogicalRect)> {
885 layout_tree
886 .nodes
887 .iter()
888 .enumerate()
889 .filter_map(|(idx, node)| {
890 let node_dom_id = node.dom_node_id?;
891 let layout_result = self.layout_results.get(&dom_id)?;
892 let node_data = &layout_result.styled_dom.node_data.as_container()[node_dom_id];
893 if matches!(node_data.get_node_type(), NodeType::IFrame(_)) {
894 let pos = calculated_positions.get(&idx).copied().unwrap_or_default();
895 let size = node.used_size.unwrap_or_default();
896 Some((node_dom_id, LogicalRect::new(pos, size)))
897 } else {
898 None
899 }
900 })
901 .collect()
902 }
903
904 pub fn resize_window(
911 &mut self,
912 styled_dom: StyledDom,
913 new_size: LogicalSize,
914 renderer_resources: &RendererResources,
915 system_callbacks: &ExternalSystemCallbacks,
916 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
917 ) -> Result<DisplayList, crate::solver3::LayoutError> {
918 let mut window_state = FullWindowState::default();
920 window_state.size.dimensions = new_size;
921
922 let dom_id = styled_dom.dom_id;
923
924 self.layout_and_generate_display_list(
927 styled_dom,
928 &window_state,
929 renderer_resources,
930 system_callbacks,
931 debug_messages,
932 )?;
933
934 self.layout_results
937 .get_mut(&dom_id)
938 .map(|result| std::mem::replace(&mut result.display_list, DisplayList::default()))
939 .ok_or(solver3::LayoutError::InvalidTree)
940 }
941
942 pub fn clear_caches(&mut self) {
944 self.layout_cache = Solver3LayoutCache {
945 tree: None,
946 calculated_positions: BTreeMap::new(),
947 viewport: None,
948 scroll_ids: BTreeMap::new(),
949 scroll_id_to_node_id: BTreeMap::new(),
950 counters: BTreeMap::new(),
951 float_cache: BTreeMap::new(),
952 };
953 self.text_cache = TextLayoutCache::new();
954 self.layout_results.clear();
955 self.scroll_manager = ScrollManager::new();
956 self.selection_manager.clear_all();
957 }
958
959 pub fn set_scroll_position(&mut self, dom_id: DomId, node_id: NodeId, scroll: ScrollPosition) {
961 #[cfg(feature = "std")]
963 let now = Instant::System(std::time::Instant::now().into());
964 #[cfg(not(feature = "std"))]
965 let now = Instant::Tick(azul_core::task::SystemTick { tick_counter: 0 });
966
967 self.scroll_manager.update_node_bounds(
968 dom_id,
969 node_id,
970 scroll.parent_rect,
971 scroll.children_rect,
972 now.clone(),
973 );
974 self.scroll_manager
975 .set_scroll_position(dom_id, node_id, scroll.children_rect.origin, now);
976 }
977
978 pub fn get_scroll_position(&self, dom_id: DomId, node_id: NodeId) -> Option<ScrollPosition> {
980 let states = self.scroll_manager.get_scroll_states_for_dom(dom_id);
981 states.get(&node_id).cloned()
982 }
983
984 pub fn set_selection(&mut self, dom_id: DomId, selection: SelectionState) {
986 self.selection_manager.set_selection(dom_id, selection);
987 }
988
989 pub fn get_selection(&self, dom_id: DomId) -> Option<&SelectionState> {
991 self.selection_manager.get_selection(&dom_id)
992 }
993
994 fn invoke_iframe_callback(
999 &mut self,
1000 parent_dom_id: DomId,
1001 node_id: NodeId,
1002 bounds: LogicalRect,
1003 window_state: &FullWindowState,
1004 renderer_resources: &RendererResources,
1005 system_callbacks: &ExternalSystemCallbacks,
1006 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1007 ) -> Option<DomId> {
1008 if let Some(msgs) = debug_messages {
1009 msgs.push(LayoutDebugMessage::info(format!(
1010 "invoke_iframe_callback called for node {:?}",
1011 node_id
1012 )));
1013 }
1014
1015 let layout_result = self.layout_results.get(&parent_dom_id)?;
1017 if let Some(msgs) = debug_messages {
1018 msgs.push(LayoutDebugMessage::info(format!(
1019 "Got layout result for parent DOM {:?}",
1020 parent_dom_id
1021 )));
1022 }
1023
1024 let node_data_container = layout_result.styled_dom.node_data.as_container();
1026 let node_data = node_data_container.get(node_id)?;
1027 if let Some(msgs) = debug_messages {
1028 msgs.push(LayoutDebugMessage::info(format!(
1029 "Got node data at index {}",
1030 node_id.index()
1031 )));
1032 }
1033
1034 let iframe_node = match node_data.get_node_type() {
1036 NodeType::IFrame(iframe) => {
1037 if let Some(msgs) = debug_messages {
1038 msgs.push(LayoutDebugMessage::info("Node is IFrame type".to_string()));
1039 }
1040 iframe.clone()
1041 }
1042 other => {
1043 if let Some(msgs) = debug_messages {
1044 msgs.push(LayoutDebugMessage::info(format!(
1045 "Node is NOT IFrame, type = {:?}",
1046 other
1047 )));
1048 }
1049 return None;
1050 }
1051 };
1052
1053 self.invoke_iframe_callback_impl(
1055 parent_dom_id,
1056 node_id,
1057 &iframe_node,
1058 bounds,
1059 window_state,
1060 renderer_resources,
1061 system_callbacks,
1062 debug_messages,
1063 )
1064 }
1065
1066 fn invoke_iframe_callback_impl(
1077 &mut self,
1078 parent_dom_id: DomId,
1079 node_id: NodeId,
1080 iframe_node: &azul_core::dom::IFrameNode,
1081 bounds: LogicalRect,
1082 window_state: &FullWindowState,
1083 renderer_resources: &RendererResources,
1084 system_callbacks: &ExternalSystemCallbacks,
1085 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1086 ) -> Option<DomId> {
1087 let now = (system_callbacks.get_system_time_fn.cb)();
1089
1090 self.scroll_manager.update_node_bounds(
1093 parent_dom_id,
1094 node_id,
1095 bounds,
1096 LogicalRect::new(LogicalPosition::zero(), bounds.size), now.clone(),
1098 );
1099
1100 let reason = match self.iframe_manager.check_reinvoke(
1103 parent_dom_id,
1104 node_id,
1105 &self.scroll_manager,
1106 bounds,
1107 ) {
1108 Some(r) => r,
1109 None => {
1110 return self
1112 .iframe_manager
1113 .get_nested_dom_id(parent_dom_id, node_id);
1114 }
1115 };
1116
1117 if let Some(msgs) = debug_messages {
1118 msgs.push(LayoutDebugMessage::info(format!(
1119 "IFrame ({:?}, {:?}) - Reason: {:?}",
1120 parent_dom_id, node_id, reason
1121 )));
1122 }
1123
1124 let scroll_offset = self
1125 .scroll_manager
1126 .get_current_offset(parent_dom_id, node_id)
1127 .unwrap_or_default();
1128 let hidpi_factor = window_state.size.get_hidpi_factor();
1129
1130 let mut callback_info = azul_core::callbacks::IFrameCallbackInfo::new(
1132 reason,
1133 &*self.font_manager.fc_cache,
1134 &self.image_cache,
1135 window_state.theme,
1136 azul_core::callbacks::HidpiAdjustedBounds {
1137 logical_size: bounds.size,
1138 hidpi_factor,
1139 },
1140 bounds.size,
1141 scroll_offset,
1142 bounds.size,
1143 LogicalPosition::zero(),
1144 );
1145
1146 let callback_data = iframe_node.refany.clone();
1148
1149 let callback_return = (iframe_node.callback.cb)(callback_data, callback_info);
1151
1152 self.iframe_manager
1154 .mark_invoked(parent_dom_id, node_id, reason);
1155
1156 let mut child_styled_dom = match callback_return.dom {
1158 azul_core::styled_dom::OptionStyledDom::Some(dom) => dom,
1159 azul_core::styled_dom::OptionStyledDom::None => {
1160 if reason == IFrameCallbackReason::InitialRender {
1162 let mut empty_dom = Dom::create_div();
1164 let empty_css = Css::empty();
1165 empty_dom.style(empty_css)
1166 } else {
1167 self.iframe_manager.update_iframe_info(
1170 parent_dom_id,
1171 node_id,
1172 callback_return.scroll_size,
1173 callback_return.virtual_scroll_size,
1174 );
1175 return self
1176 .iframe_manager
1177 .get_nested_dom_id(parent_dom_id, node_id);
1178 }
1179 }
1180 };
1181
1182 let child_dom_id = self
1184 .iframe_manager
1185 .get_or_create_nested_dom_id(parent_dom_id, node_id);
1186 child_styled_dom.dom_id = child_dom_id;
1187
1188 self.iframe_manager.update_iframe_info(
1190 parent_dom_id,
1191 node_id,
1192 callback_return.scroll_size,
1193 callback_return.virtual_scroll_size,
1194 );
1195
1196 self.layout_dom_recursive(
1200 child_styled_dom,
1201 window_state,
1202 renderer_resources,
1203 system_callbacks,
1204 debug_messages,
1205 )
1206 .ok()?;
1207
1208 Some(child_dom_id)
1209 }
1210
1211 pub fn get_node_size(&self, node_id: DomNodeId) -> Option<LogicalSize> {
1215 let layout_result = self.layout_results.get(&node_id.dom)?;
1216 let nid = node_id.node.into_crate_internal()?;
1217 let layout_indices = layout_result.layout_tree.dom_to_layout.get(&nid)?;
1219 let layout_index = *layout_indices.first()?;
1220 let layout_node = layout_result.layout_tree.get(layout_index)?;
1221 layout_node.used_size
1222 }
1223
1224 pub fn get_node_position(&self, node_id: DomNodeId) -> Option<LogicalPosition> {
1226 let layout_result = self.layout_results.get(&node_id.dom)?;
1227 let nid = node_id.node.into_crate_internal()?;
1228 let layout_indices = layout_result.layout_tree.dom_to_layout.get(&nid)?;
1230 let layout_index = *layout_indices.first()?;
1231 let position = layout_result.calculated_positions.get(&layout_index)?;
1232 Some(*position)
1233 }
1234
1235 pub fn get_node_hit_test_bounds(&self, node_id: DomNodeId) -> Option<LogicalRect> {
1241 use crate::solver3::display_list::DisplayListItem;
1242
1243 let layout_result = self.layout_results.get(&node_id.dom)?;
1244 let nid = node_id.node.into_crate_internal()?;
1245
1246 let styled_nodes = layout_result.styled_dom.styled_nodes.as_container();
1248 let tag_id = styled_nodes.get(nid)?.tag_id.into_option()?.inner;
1249
1250 for item in &layout_result.display_list.items {
1253 if let DisplayListItem::HitTestArea { bounds, tag } = item {
1254 if tag.0 == tag_id && bounds.size.width > 0.0 && bounds.size.height > 0.0 {
1255 return Some(*bounds);
1256 }
1257 }
1258 }
1259 None
1260 }
1261
1262 pub fn get_parent(&self, node_id: DomNodeId) -> Option<DomNodeId> {
1264 let layout_result = self.layout_results.get(&node_id.dom)?;
1265 let nid = node_id.node.into_crate_internal()?;
1266 let parent_id = layout_result
1267 .styled_dom
1268 .node_hierarchy
1269 .as_container()
1270 .get(nid)?
1271 .parent_id()?;
1272 Some(DomNodeId {
1273 dom: node_id.dom,
1274 node: NodeHierarchyItemId::from_crate_internal(Some(parent_id)),
1275 })
1276 }
1277
1278 pub fn get_first_child(&self, node_id: DomNodeId) -> Option<DomNodeId> {
1280 let layout_result = self.layout_results.get(&node_id.dom)?;
1281 let nid = node_id.node.into_crate_internal()?;
1282 let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
1283 let hierarchy_item = node_hierarchy.get(nid)?;
1284 let first_child_id = hierarchy_item.first_child_id(nid)?;
1285 Some(DomNodeId {
1286 dom: node_id.dom,
1287 node: NodeHierarchyItemId::from_crate_internal(Some(first_child_id)),
1288 })
1289 }
1290
1291 pub fn get_next_sibling(&self, node_id: DomNodeId) -> Option<DomNodeId> {
1293 let layout_result = self.layout_results.get(&node_id.dom)?;
1294 let nid = node_id.node.into_crate_internal()?;
1295 let next_sibling_id = layout_result
1296 .styled_dom
1297 .node_hierarchy
1298 .as_container()
1299 .get(nid)?
1300 .next_sibling_id()?;
1301 Some(DomNodeId {
1302 dom: node_id.dom,
1303 node: NodeHierarchyItemId::from_crate_internal(Some(next_sibling_id)),
1304 })
1305 }
1306
1307 pub fn get_previous_sibling(&self, node_id: DomNodeId) -> Option<DomNodeId> {
1309 let layout_result = self.layout_results.get(&node_id.dom)?;
1310 let nid = node_id.node.into_crate_internal()?;
1311 let prev_sibling_id = layout_result
1312 .styled_dom
1313 .node_hierarchy
1314 .as_container()
1315 .get(nid)?
1316 .previous_sibling_id()?;
1317 Some(DomNodeId {
1318 dom: node_id.dom,
1319 node: NodeHierarchyItemId::from_crate_internal(Some(prev_sibling_id)),
1320 })
1321 }
1322
1323 pub fn get_last_child(&self, node_id: DomNodeId) -> Option<DomNodeId> {
1325 let layout_result = self.layout_results.get(&node_id.dom)?;
1326 let nid = node_id.node.into_crate_internal()?;
1327 let last_child_id = layout_result
1328 .styled_dom
1329 .node_hierarchy
1330 .as_container()
1331 .get(nid)?
1332 .last_child_id()?;
1333 Some(DomNodeId {
1334 dom: node_id.dom,
1335 node: NodeHierarchyItemId::from_crate_internal(Some(last_child_id)),
1336 })
1337 }
1338
1339 pub fn scan_used_fonts(&self) -> BTreeSet<FontKey> {
1341 let mut fonts = BTreeSet::new();
1342 for (_dom_id, layout_result) in &self.layout_results {
1343 }
1346 fonts
1347 }
1348
1349 pub fn scan_used_images(&self, _css_image_cache: &ImageCache) -> BTreeSet<ImageRefHash> {
1351 let mut images = BTreeSet::new();
1352 for (_dom_id, layout_result) in &self.layout_results {
1353 }
1356 images
1357 }
1358
1359 fn get_nested_scroll_states(
1361 &self,
1362 dom_id: DomId,
1363 ) -> BTreeMap<DomId, BTreeMap<NodeHierarchyItemId, ScrollPosition>> {
1364 let mut nested = BTreeMap::new();
1365 let scroll_states = self.scroll_manager.get_scroll_states_for_dom(dom_id);
1366 let mut inner = BTreeMap::new();
1367 for (node_id, scroll_pos) in scroll_states {
1368 inner.insert(
1369 NodeHierarchyItemId::from_crate_internal(Some(node_id)),
1370 scroll_pos,
1371 );
1372 }
1373 nested.insert(dom_id, inner);
1374 nested
1375 }
1376
1377 pub fn scroll_node_into_view(
1396 &mut self,
1397 node_id: DomNodeId,
1398 options: crate::managers::scroll_into_view::ScrollIntoViewOptions,
1399 now: azul_core::task::Instant,
1400 ) -> Vec<crate::managers::scroll_into_view::ScrollAdjustment> {
1401 crate::managers::scroll_into_view::scroll_node_into_view(
1402 node_id,
1403 &self.layout_results,
1404 &mut self.scroll_manager,
1405 options,
1406 now,
1407 )
1408 }
1409
1410 pub fn scroll_cursor_into_view(
1415 &mut self,
1416 cursor_rect: LogicalRect,
1417 node_id: DomNodeId,
1418 options: crate::managers::scroll_into_view::ScrollIntoViewOptions,
1419 now: azul_core::task::Instant,
1420 ) -> Vec<crate::managers::scroll_into_view::ScrollAdjustment> {
1421 crate::managers::scroll_into_view::scroll_cursor_into_view(
1422 cursor_rect,
1423 node_id,
1424 &self.layout_results,
1425 &mut self.scroll_manager,
1426 options,
1427 now,
1428 )
1429 }
1430
1431 pub fn add_timer(&mut self, timer_id: TimerId, timer: Timer) {
1435 self.timers.insert(timer_id, timer);
1436 }
1437
1438 pub fn remove_timer(&mut self, timer_id: &TimerId) -> Option<Timer> {
1440 self.timers.remove(timer_id)
1441 }
1442
1443 pub fn get_timer(&self, timer_id: &TimerId) -> Option<&Timer> {
1445 self.timers.get(timer_id)
1446 }
1447
1448 pub fn get_timer_mut(&mut self, timer_id: &TimerId) -> Option<&mut Timer> {
1450 self.timers.get_mut(timer_id)
1451 }
1452
1453 pub fn get_timer_ids(&self) -> TimerIdVec {
1455 self.timers.keys().copied().collect::<Vec<_>>().into()
1456 }
1457
1458 pub fn tick_timers(&mut self, current_time: azul_core::task::Instant) -> Vec<TimerId> {
1461 let mut ready_timers = Vec::new();
1462
1463 for (timer_id, timer) in &mut self.timers {
1464 ready_timers.push(*timer_id);
1469 }
1470
1471 ready_timers
1472 }
1473
1474 pub fn time_until_next_timer_ms(
1483 &self,
1484 get_system_time_fn: &azul_core::task::GetSystemTimeCallback,
1485 ) -> Option<u64> {
1486 if self.timers.is_empty() {
1487 return None; }
1489
1490 let now = (get_system_time_fn.cb)();
1491 let mut min_ms: Option<u64> = None;
1492
1493 for timer in self.timers.values() {
1494 let next_run = timer.instant_of_next_run();
1495
1496 let ms_until = if next_run < now {
1498 0 } else {
1500 duration_to_millis(next_run.duration_since(&now))
1501 };
1502
1503 min_ms = Some(match min_ms {
1504 Some(current_min) => current_min.min(ms_until),
1505 None => ms_until,
1506 });
1507 }
1508
1509 min_ms
1510 }
1511
1512 pub fn add_thread(&mut self, thread_id: ThreadId, thread: Thread) {
1516 self.threads.insert(thread_id, thread);
1517 }
1518
1519 pub fn remove_thread(&mut self, thread_id: &ThreadId) -> Option<Thread> {
1521 self.threads.remove(thread_id)
1522 }
1523
1524 pub fn get_thread(&self, thread_id: &ThreadId) -> Option<&Thread> {
1526 self.threads.get(thread_id)
1527 }
1528
1529 pub fn get_thread_mut(&mut self, thread_id: &ThreadId) -> Option<&mut Thread> {
1531 self.threads.get_mut(thread_id)
1532 }
1533
1534 pub fn get_thread_ids(&self) -> ThreadIdVec {
1536 self.threads.keys().copied().collect::<Vec<_>>().into()
1537 }
1538
1539 pub fn create_cursor_blink_timer(&self, _window_state: &FullWindowState) -> crate::timer::Timer {
1547 use azul_core::task::{Duration, SystemTimeDiff};
1548 use crate::timer::{Timer, TimerCallback};
1549 use azul_core::refany::RefAny;
1550
1551 let interval_ms = crate::managers::cursor::CURSOR_BLINK_INTERVAL_MS;
1552
1553 let refany = RefAny::new(());
1556
1557 Timer {
1558 refany,
1559 node_id: None.into(),
1560 created: azul_core::task::Instant::now(),
1561 run_count: 0,
1562 last_run: azul_core::task::OptionInstant::None,
1563 delay: azul_core::task::OptionDuration::None,
1564 interval: azul_core::task::OptionDuration::Some(Duration::System(SystemTimeDiff::from_millis(interval_ms))),
1565 timeout: azul_core::task::OptionDuration::None,
1566 callback: TimerCallback::create(cursor_blink_timer_callback),
1567 }
1568 }
1569
1570 pub fn scroll_active_cursor_into_view(&mut self, result: &mut CallbackChangeResult) {
1575 use crate::managers::scroll_into_view;
1576
1577 let focused_node = match self.focus_manager.get_focused_node() {
1579 Some(node) => *node,
1580 None => return,
1581 };
1582
1583 let Some(node_id_internal) = focused_node.node.into_crate_internal() else {
1584 return;
1585 };
1586
1587 if !self.is_node_contenteditable_internal(focused_node.dom, node_id_internal) {
1589 return;
1590 }
1591
1592 let cursor_location = match self.cursor_manager.get_cursor_location() {
1594 Some(loc) if loc.dom_id == focused_node.dom && loc.node_id == node_id_internal => loc,
1595 _ => return,
1596 };
1597
1598 let cursor = match self.cursor_manager.get_cursor() {
1600 Some(c) => c.clone(),
1601 None => return,
1602 };
1603
1604 let layout = match self.get_inline_layout_for_node(focused_node.dom, node_id_internal) {
1606 Some(l) => l,
1607 None => return,
1608 };
1609
1610 let cursor_rect = match layout.get_cursor_rect(&cursor) {
1612 Some(r) => r,
1613 None => return,
1614 };
1615
1616 let now = azul_core::task::Instant::now();
1618 let options = scroll_into_view::ScrollIntoViewOptions::nearest();
1619
1620 let adjustments = scroll_into_view::scroll_rect_into_view(
1622 cursor_rect,
1623 focused_node.dom,
1624 node_id_internal,
1625 &self.layout_results,
1626 &mut self.scroll_manager,
1627 options,
1628 now,
1629 );
1630
1631 for adj in adjustments {
1633 let current_pos = self.scroll_manager
1634 .get_current_offset(adj.scroll_container_dom_id, adj.scroll_container_node_id)
1635 .unwrap_or(LogicalPosition::zero());
1636
1637 let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(adj.scroll_container_node_id));
1638 result
1639 .nodes_scrolled
1640 .entry(adj.scroll_container_dom_id)
1641 .or_insert_with(BTreeMap::new)
1642 .insert(hierarchy_id, current_pos);
1643 }
1644 }
1645
1646 fn is_node_contenteditable_internal(&self, dom_id: DomId, node_id: NodeId) -> bool {
1648 use crate::solver3::getters::is_node_contenteditable;
1649
1650 let Some(layout_result) = self.layout_results.get(&dom_id) else {
1651 return false;
1652 };
1653
1654 is_node_contenteditable(&layout_result.styled_dom, node_id)
1655 }
1656
1657 fn is_node_contenteditable_inherited_internal(&self, dom_id: DomId, node_id: NodeId) -> bool {
1663 use crate::solver3::getters::is_node_contenteditable_inherited;
1664
1665 let Some(layout_result) = self.layout_results.get(&dom_id) else {
1666 return false;
1667 };
1668
1669 is_node_contenteditable_inherited(&layout_result.styled_dom, node_id)
1670 }
1671
1672 pub fn handle_focus_change_for_cursor_blink(
1692 &mut self,
1693 new_focus: Option<azul_core::dom::DomNodeId>,
1694 current_window_state: &FullWindowState,
1695 ) -> CursorBlinkTimerAction {
1696 let contenteditable_info = match new_focus {
1699 Some(focus_node) => {
1700 if let Some(node_id) = focus_node.node.into_crate_internal() {
1701 if self.is_node_contenteditable_inherited_internal(focus_node.dom, node_id) {
1703 let text_node_id = self.find_last_text_child(focus_node.dom, node_id)
1705 .unwrap_or(node_id);
1706 Some((focus_node.dom, node_id, text_node_id))
1707 } else {
1708 None
1709 }
1710 } else {
1711 None
1712 }
1713 }
1714 None => None,
1715 };
1716
1717 let timer_was_active = self.cursor_manager.is_blink_timer_active();
1719
1720 if let Some((dom_id, container_node_id, text_node_id)) = contenteditable_info {
1721
1722 self.focus_manager.set_pending_contenteditable_focus(
1725 dom_id,
1726 container_node_id,
1727 text_node_id,
1728 );
1729
1730 let now = azul_core::task::Instant::now();
1732 self.cursor_manager.reset_blink_on_input(now);
1733 self.cursor_manager.set_blink_timer_active(true);
1734
1735 if !timer_was_active {
1736 let timer = self.create_cursor_blink_timer(current_window_state);
1738 return CursorBlinkTimerAction::Start(timer);
1739 } else {
1740 return CursorBlinkTimerAction::NoChange;
1742 }
1743 } else {
1744 self.cursor_manager.clear();
1748 self.focus_manager.clear_pending_contenteditable_focus();
1749
1750 if timer_was_active {
1751 self.cursor_manager.set_blink_timer_active(false);
1753 return CursorBlinkTimerAction::Stop;
1754 } else {
1755 return CursorBlinkTimerAction::NoChange;
1756 }
1757 }
1758 }
1759
1760 pub fn finalize_pending_focus_changes(&mut self) -> bool {
1782 let pending = match self.focus_manager.take_pending_contenteditable_focus() {
1784 Some(p) => p,
1785 None => return false,
1786 };
1787
1788 let text_layout = self.get_inline_layout_for_node(pending.dom_id, pending.text_node_id).cloned();
1790
1791 self.cursor_manager.initialize_cursor_at_end(
1793 pending.dom_id,
1794 pending.text_node_id,
1795 text_layout.as_ref(),
1796 )
1797 }
1798
1799 pub fn apply_callback_changes(
1809 &mut self,
1810 changes: Vec<crate::callbacks::CallbackChange>,
1811 current_window_state: &FullWindowState,
1812 image_cache: &mut ImageCache,
1813 system_fonts: &mut FcFontCache,
1814 ) -> CallbackChangeResult {
1815 use crate::callbacks::CallbackChange;
1816
1817 let mut result = CallbackChangeResult {
1818 modified_window_state: current_window_state.clone(),
1819 ..Default::default()
1820 };
1821 for change in changes {
1822 match change {
1823 CallbackChange::ModifyWindowState { state } => {
1824 result.modified_window_state = state;
1825 }
1826 CallbackChange::QueueWindowStateSequence { states } => {
1827 result.queued_window_states.extend(states);
1831 }
1832 CallbackChange::CreateNewWindow { options } => {
1833 result.windows_created.push(options);
1834 }
1835 CallbackChange::CloseWindow => {
1836 result.modified_window_state.flags.close_requested = true;
1838 }
1839 CallbackChange::SetFocusTarget { target } => {
1840 result.focus_target = Some(target);
1841 }
1842 CallbackChange::StopPropagation => {
1843 result.stop_propagation = true;
1844 }
1845 CallbackChange::PreventDefault => {
1846 result.prevent_default = true;
1847 }
1848 CallbackChange::AddTimer { timer_id, timer } => {
1849 result.timers.insert(timer_id, timer);
1850 }
1851 CallbackChange::RemoveTimer { timer_id } => {
1852 result.timers_removed.insert(timer_id);
1853 }
1854 CallbackChange::AddThread { thread_id, thread } => {
1855 result.threads.insert(thread_id, thread);
1856 }
1857 CallbackChange::RemoveThread { thread_id } => {
1858 result.threads_removed.insert(thread_id);
1859 }
1860 CallbackChange::ChangeNodeText { node_id, text } => {
1861 let dom_id = node_id.dom;
1862 let internal_node_id = match node_id.node.into_crate_internal() {
1863 Some(id) => id,
1864 None => continue,
1865 };
1866 result
1867 .words_changed
1868 .entry(dom_id)
1869 .or_insert_with(BTreeMap::new)
1870 .insert(internal_node_id, text);
1871 }
1872 CallbackChange::ChangeNodeImage {
1873 dom_id,
1874 node_id,
1875 image,
1876 update_type,
1877 } => {
1878 result
1879 .images_changed
1880 .entry(dom_id)
1881 .or_insert_with(BTreeMap::new)
1882 .insert(node_id, (image, update_type));
1883 }
1884 CallbackChange::UpdateImageCallback { dom_id, node_id } => {
1885 result
1886 .image_callbacks_changed
1887 .entry(dom_id)
1888 .or_insert_with(FastBTreeSet::new)
1889 .insert(node_id);
1890 }
1891 CallbackChange::UpdateIFrame { dom_id, node_id } => {
1892 result
1893 .iframes_to_update
1894 .entry(dom_id)
1895 .or_insert_with(FastBTreeSet::new)
1896 .insert(node_id);
1897 }
1898 CallbackChange::ChangeNodeImageMask {
1899 dom_id,
1900 node_id,
1901 mask,
1902 } => {
1903 result
1904 .image_masks_changed
1905 .entry(dom_id)
1906 .or_insert_with(BTreeMap::new)
1907 .insert(node_id, mask);
1908 }
1909 CallbackChange::ChangeNodeCssProperties {
1910 dom_id,
1911 node_id,
1912 properties,
1913 } => {
1914 result
1915 .css_properties_changed
1916 .entry(dom_id)
1917 .or_insert_with(BTreeMap::new)
1918 .insert(node_id, properties);
1919 }
1920 CallbackChange::ScrollTo {
1921 dom_id,
1922 node_id,
1923 position,
1924 } => {
1925 result
1926 .nodes_scrolled
1927 .entry(dom_id)
1928 .or_insert_with(BTreeMap::new)
1929 .insert(node_id, position);
1930 }
1931 CallbackChange::ScrollIntoView { node_id, options } => {
1932 use crate::managers::scroll_into_view;
1934 let now = azul_core::task::Instant::now();
1935 let adjustments = scroll_into_view::scroll_node_into_view(
1936 node_id,
1937 &self.layout_results,
1938 &mut self.scroll_manager,
1939 options,
1940 now,
1941 );
1942 for adj in adjustments {
1946 let current_pos = self.scroll_manager
1948 .get_current_offset(adj.scroll_container_dom_id, adj.scroll_container_node_id)
1949 .unwrap_or(LogicalPosition::zero());
1950
1951 let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(adj.scroll_container_node_id));
1952 result
1953 .nodes_scrolled
1954 .entry(adj.scroll_container_dom_id)
1955 .or_insert_with(BTreeMap::new)
1956 .insert(hierarchy_id, current_pos);
1957 }
1958 }
1959 CallbackChange::AddImageToCache { id, image } => {
1960 image_cache.add_css_image_id(id, image);
1961 }
1962 CallbackChange::RemoveImageFromCache { id } => {
1963 image_cache.delete_css_image_id(&id);
1964 }
1965 CallbackChange::ReloadSystemFonts => {
1966 *system_fonts = FcFontCache::build();
1967 }
1968 CallbackChange::OpenMenu { menu, position } => {
1969 result.menus_to_open.push((menu, position));
1970 }
1971 CallbackChange::ShowTooltip { text, position } => {
1972 result.tooltips_to_show.push((text, position));
1973 }
1974 CallbackChange::HideTooltip => {
1975 result.hide_tooltip = true;
1976 }
1977 CallbackChange::InsertText {
1978 dom_id,
1979 node_id,
1980 text,
1981 } => {
1982 let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(node_id));
1984 let dom_node_id = DomNodeId {
1985 dom: dom_id,
1986 node: hierarchy_id,
1987 };
1988
1989 let old_inline_content = self.get_text_before_textinput(dom_id, node_id);
1991 let old_text = self.extract_text_from_inline_content(&old_inline_content);
1992
1993 use crate::managers::text_input::TextInputSource;
1995 self.text_input_manager.record_input(
1996 dom_node_id,
1997 text.to_string(),
1998 old_text,
1999 TextInputSource::Programmatic,
2000 );
2001 }
2002 CallbackChange::DeleteBackward { dom_id, node_id } => {
2003 if let Some(cursor) = self.cursor_manager.get_cursor() {
2005 let content = self.get_text_before_textinput(dom_id, node_id);
2007
2008 use crate::text3::edit::{delete_backward, TextEdit};
2011 let mut new_content = content.clone();
2012 let (updated_content, new_cursor) =
2013 delete_backward(&mut new_content, cursor);
2014
2015 self.cursor_manager
2017 .move_cursor_to(new_cursor, dom_id, node_id);
2018
2019 self.update_text_cache_after_edit(dom_id, node_id, updated_content);
2021
2022 let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(node_id));
2024 let dom_node_id = DomNodeId {
2025 dom: dom_id,
2026 node: hierarchy_id,
2027 };
2028 }
2030 }
2031 CallbackChange::DeleteForward { dom_id, node_id } => {
2032 if let Some(cursor) = self.cursor_manager.get_cursor() {
2034 let content = self.get_text_before_textinput(dom_id, node_id);
2036
2037 use crate::text3::edit::{delete_forward, TextEdit};
2040 let mut new_content = content.clone();
2041 let (updated_content, new_cursor) =
2042 delete_forward(&mut new_content, cursor);
2043
2044 self.cursor_manager
2046 .move_cursor_to(new_cursor, dom_id, node_id);
2047
2048 self.update_text_cache_after_edit(dom_id, node_id, updated_content);
2050 }
2051 }
2052 CallbackChange::MoveCursor {
2053 dom_id,
2054 node_id,
2055 cursor,
2056 } => {
2057 self.cursor_manager.move_cursor_to(cursor, dom_id, node_id);
2059 }
2060 CallbackChange::SetSelection {
2061 dom_id,
2062 node_id,
2063 selection,
2064 } => {
2065 let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(node_id));
2067 let dom_node_id = DomNodeId {
2068 dom: dom_id,
2069 node: hierarchy_id,
2070 };
2071
2072 match selection {
2073 Selection::Cursor(cursor) => {
2074 self.cursor_manager.move_cursor_to(cursor, dom_id, node_id);
2075 self.selection_manager.clear_all();
2076 }
2077 Selection::Range(range) => {
2078 self.cursor_manager
2079 .move_cursor_to(range.start, dom_id, node_id);
2080 }
2083 }
2084 }
2085 CallbackChange::SetTextChangeset { changeset } => {
2086 self.text_input_manager.pending_changeset = Some(changeset);
2089 }
2090 CallbackChange::MoveCursorLeft {
2092 dom_id,
2093 node_id,
2094 extend_selection,
2095 } => {
2096 if let Some(new_cursor) =
2097 self.move_cursor_in_node(dom_id, node_id, |layout, cursor| {
2098 layout.move_cursor_left(*cursor, &mut None)
2099 })
2100 {
2101 self.handle_cursor_movement(dom_id, node_id, new_cursor, extend_selection);
2102 }
2103 }
2104 CallbackChange::MoveCursorRight {
2105 dom_id,
2106 node_id,
2107 extend_selection,
2108 } => {
2109 if let Some(new_cursor) =
2110 self.move_cursor_in_node(dom_id, node_id, |layout, cursor| {
2111 layout.move_cursor_right(*cursor, &mut None)
2112 })
2113 {
2114 self.handle_cursor_movement(dom_id, node_id, new_cursor, extend_selection);
2115 }
2116 }
2117 CallbackChange::MoveCursorUp {
2118 dom_id,
2119 node_id,
2120 extend_selection,
2121 } => {
2122 if let Some(new_cursor) =
2123 self.move_cursor_in_node(dom_id, node_id, |layout, cursor| {
2124 layout.move_cursor_up(*cursor, &mut None, &mut None)
2125 })
2126 {
2127 self.handle_cursor_movement(dom_id, node_id, new_cursor, extend_selection);
2128 }
2129 }
2130 CallbackChange::MoveCursorDown {
2131 dom_id,
2132 node_id,
2133 extend_selection,
2134 } => {
2135 if let Some(new_cursor) =
2136 self.move_cursor_in_node(dom_id, node_id, |layout, cursor| {
2137 layout.move_cursor_down(*cursor, &mut None, &mut None)
2138 })
2139 {
2140 self.handle_cursor_movement(dom_id, node_id, new_cursor, extend_selection);
2141 }
2142 }
2143 CallbackChange::MoveCursorToLineStart {
2144 dom_id,
2145 node_id,
2146 extend_selection,
2147 } => {
2148 if let Some(new_cursor) =
2149 self.move_cursor_in_node(dom_id, node_id, |layout, cursor| {
2150 layout.move_cursor_to_line_start(*cursor, &mut None)
2151 })
2152 {
2153 self.handle_cursor_movement(dom_id, node_id, new_cursor, extend_selection);
2154 }
2155 }
2156 CallbackChange::MoveCursorToLineEnd {
2157 dom_id,
2158 node_id,
2159 extend_selection,
2160 } => {
2161 if let Some(new_cursor) =
2162 self.move_cursor_in_node(dom_id, node_id, |layout, cursor| {
2163 layout.move_cursor_to_line_end(*cursor, &mut None)
2164 })
2165 {
2166 self.handle_cursor_movement(dom_id, node_id, new_cursor, extend_selection);
2167 }
2168 }
2169 CallbackChange::MoveCursorToDocumentStart {
2170 dom_id,
2171 node_id,
2172 extend_selection,
2173 } => {
2174 if let Some(new_cursor) = self.get_inline_layout_for_node(dom_id, node_id) {
2176 if let Some(first_cluster) = new_cursor
2177 .items
2178 .first()
2179 .and_then(|item| item.item.as_cluster())
2180 {
2181 let doc_start_cursor = TextCursor {
2182 cluster_id: first_cluster.source_cluster_id,
2183 affinity: CursorAffinity::Leading,
2184 };
2185 self.handle_cursor_movement(
2186 dom_id,
2187 node_id,
2188 doc_start_cursor,
2189 extend_selection,
2190 );
2191 }
2192 }
2193 }
2194 CallbackChange::MoveCursorToDocumentEnd {
2195 dom_id,
2196 node_id,
2197 extend_selection,
2198 } => {
2199 if let Some(layout) = self.get_inline_layout_for_node(dom_id, node_id) {
2201 if let Some(last_cluster) =
2202 layout.items.last().and_then(|item| item.item.as_cluster())
2203 {
2204 let doc_end_cursor = TextCursor {
2205 cluster_id: last_cluster.source_cluster_id,
2206 affinity: CursorAffinity::Trailing,
2207 };
2208 self.handle_cursor_movement(
2209 dom_id,
2210 node_id,
2211 doc_end_cursor,
2212 extend_selection,
2213 );
2214 }
2215 }
2216 }
2217 CallbackChange::SetCopyContent { target, content } => {
2219 self.clipboard_manager.set_copy_content(content);
2222 }
2223 CallbackChange::SetCutContent { target, content } => {
2224 self.clipboard_manager.set_copy_content(content);
2226 }
2227 CallbackChange::SetSelectAllRange { target, range } => {
2228 if let Some(node_id_internal) = target.node.into_crate_internal() {
2231 let dom_node_id = azul_core::dom::DomNodeId {
2232 dom: target.dom,
2233 node: target.node,
2234 };
2235 self.selection_manager
2236 .set_range(target.dom, dom_node_id, range);
2237 }
2238 }
2239 CallbackChange::RequestHitTestUpdate { position } => {
2240 result.hit_test_update_requested = Some(position);
2243 }
2244 CallbackChange::ProcessTextSelectionClick { position, time_ms } => {
2245 let _ = self.process_mouse_click_for_selection(position, time_ms);
2249 }
2250 CallbackChange::SetCursorVisibility { visible: _ } => {
2251 let now = azul_core::task::Instant::now();
2253 if self.cursor_manager.should_blink(&now) {
2254 self.cursor_manager.toggle_visibility();
2256 } else {
2257 self.cursor_manager.set_visibility(true);
2259 }
2260 }
2261 CallbackChange::ResetCursorBlink => {
2262 let now = azul_core::task::Instant::now();
2264 self.cursor_manager.reset_blink_on_input(now);
2265 }
2266 CallbackChange::StartCursorBlinkTimer => {
2267 if !self.cursor_manager.is_blink_timer_active() {
2269 let timer = self.create_cursor_blink_timer(current_window_state);
2270 result.timers.insert(azul_core::task::CURSOR_BLINK_TIMER_ID, timer);
2271 self.cursor_manager.set_blink_timer_active(true);
2272 }
2273 }
2274 CallbackChange::StopCursorBlinkTimer => {
2275 if self.cursor_manager.is_blink_timer_active() {
2277 result.timers_removed.insert(azul_core::task::CURSOR_BLINK_TIMER_ID);
2278 self.cursor_manager.set_blink_timer_active(false);
2279 }
2280 }
2281 CallbackChange::ScrollActiveCursorIntoView => {
2282 self.scroll_active_cursor_into_view(&mut result);
2284 }
2285 CallbackChange::CreateTextInput { text } => {
2286 println!("[CreateTextInput] Processing text: '{}'", text.as_str());
2289
2290 let affected_nodes = self.process_text_input(text.as_str());
2292 println!("[CreateTextInput] process_text_input returned {} affected nodes", affected_nodes.len());
2293
2294 for (node, (events, _)) in affected_nodes {
2297 result.text_input_triggered.push((node, events));
2298 }
2299 }
2300 }
2301 }
2302
2303 self.sync_cursor_to_selection_manager();
2306
2307 result
2308 }
2309
2310 fn get_inline_layout_for_node(
2320 &self,
2321 dom_id: DomId,
2322 node_id: NodeId,
2323 ) -> Option<&Arc<UnifiedLayout>> {
2324 let layout_result = self.layout_results.get(&dom_id)?;
2325
2326 let layout_indices = layout_result.layout_tree.dom_to_layout.get(&node_id)?;
2327 let layout_index = *layout_indices.first()?;
2328
2329 layout_result.layout_tree.get_inline_layout_for_node(layout_index)
2331 }
2332
2333 fn move_cursor_in_node<F>(
2335 &self,
2336 dom_id: DomId,
2337 node_id: NodeId,
2338 movement_fn: F,
2339 ) -> Option<TextCursor>
2340 where
2341 F: FnOnce(&UnifiedLayout, &TextCursor) -> TextCursor,
2342 {
2343 let current_cursor = self.cursor_manager.get_cursor()?;
2344 let layout = self.get_inline_layout_for_node(dom_id, node_id)?;
2345
2346 let new_cursor = movement_fn(layout, current_cursor);
2347
2348 if new_cursor != *current_cursor {
2350 Some(new_cursor)
2351 } else {
2352 None
2353 }
2354 }
2355
2356 fn handle_cursor_movement(
2358 &mut self,
2359 dom_id: DomId,
2360 node_id: NodeId,
2361 new_cursor: TextCursor,
2362 extend_selection: bool,
2363 ) {
2364 if extend_selection {
2365 if let Some(old_cursor) = self.cursor_manager.get_cursor() {
2367 let dom_node_id = azul_core::dom::DomNodeId {
2369 dom: dom_id,
2370 node: NodeHierarchyItemId::from_crate_internal(Some(node_id)),
2371 };
2372
2373 let selection_range = if new_cursor.cluster_id.start_byte_in_run
2375 < old_cursor.cluster_id.start_byte_in_run
2376 {
2377 SelectionRange {
2379 start: new_cursor,
2380 end: *old_cursor,
2381 }
2382 } else {
2383 SelectionRange {
2385 start: *old_cursor,
2386 end: new_cursor,
2387 }
2388 };
2389
2390 self.selection_manager
2392 .set_range(dom_id, dom_node_id, selection_range);
2393 }
2394
2395 self.cursor_manager
2397 .move_cursor_to(new_cursor, dom_id, node_id);
2398 } else {
2399 self.cursor_manager
2401 .move_cursor_to(new_cursor, dom_id, node_id);
2402
2403 self.selection_manager.clear_selection(&dom_id);
2405 }
2406 }
2407
2408 pub fn get_gpu_cache(&self, dom_id: &DomId) -> Option<&GpuValueCache> {
2412 self.gpu_state_manager.caches.get(dom_id)
2413 }
2414
2415 pub fn get_gpu_cache_mut(&mut self, dom_id: &DomId) -> Option<&mut GpuValueCache> {
2417 self.gpu_state_manager.caches.get_mut(dom_id)
2418 }
2419
2420 pub fn get_or_create_gpu_cache(&mut self, dom_id: DomId) -> &mut GpuValueCache {
2422 self.gpu_state_manager
2423 .caches
2424 .entry(dom_id)
2425 .or_insert_with(GpuValueCache::default)
2426 }
2427
2428 pub fn get_layout_result(&self, dom_id: &DomId) -> Option<&DomLayoutResult> {
2432 self.layout_results.get(dom_id)
2433 }
2434
2435 pub fn get_layout_result_mut(&mut self, dom_id: &DomId) -> Option<&mut DomLayoutResult> {
2437 self.layout_results.get_mut(dom_id)
2438 }
2439
2440 pub fn get_dom_ids(&self) -> DomIdVec {
2442 self.layout_results
2443 .keys()
2444 .copied()
2445 .collect::<Vec<_>>()
2446 .into()
2447 }
2448
2449 pub fn compute_cursor_type_hit_test(
2456 &self,
2457 hit_test: &crate::hit_test::FullHitTest,
2458 ) -> crate::hit_test::CursorTypeHitTest {
2459 crate::hit_test::CursorTypeHitTest::new(hit_test, self)
2460 }
2461
2462 fn calculate_scrollbar_opacity(
2489 last_activity: Option<Instant>,
2490 now: Instant,
2491 fade_delay: Duration,
2492 fade_duration: Duration,
2493 ) -> f32 {
2494 let Some(last_activity) = last_activity else {
2495 return 0.0;
2496 };
2497
2498 let time_since_activity = now.duration_since(&last_activity);
2499
2500 if time_since_activity.div(&fade_delay) < 1.0 {
2502 return 1.0;
2503 }
2504
2505 let time_into_fade_ms = time_since_activity.div(&fade_delay) - 1.0;
2507 let fade_progress = (time_into_fade_ms * fade_duration.div(&fade_duration)).min(1.0);
2508
2509 (1.0 - fade_progress).max(0.0)
2511 }
2512
2513 pub fn synchronize_scrollbar_opacity(
2518 gpu_state_manager: &mut GpuStateManager,
2519 scroll_manager: &ScrollManager,
2520 dom_id: DomId,
2521 layout_tree: &LayoutTree,
2522 system_callbacks: &ExternalSystemCallbacks,
2523 fade_delay: azul_core::task::Duration,
2524 fade_duration: azul_core::task::Duration,
2525 ) -> Vec<azul_core::gpu::GpuScrollbarOpacityEvent> {
2526 let mut events = Vec::new();
2527 let gpu_cache = gpu_state_manager.caches.entry(dom_id).or_default();
2528
2529 let now = (system_callbacks.get_system_time_fn.cb)();
2531
2532 for (node_idx, node) in layout_tree.nodes.iter().enumerate() {
2534 let scrollbar_info = match &node.scrollbar_info {
2536 Some(info) => info,
2537 None => continue,
2538 };
2539
2540 let node_id = match node.dom_node_id {
2541 Some(nid) => nid,
2542 None => continue, };
2544
2545 let vertical_opacity = if scrollbar_info.needs_vertical {
2547 Self::calculate_scrollbar_opacity(
2548 scroll_manager.get_last_activity_time(dom_id, node_id),
2549 now.clone(),
2550 fade_delay,
2551 fade_duration,
2552 )
2553 } else {
2554 0.0
2555 };
2556
2557 let horizontal_opacity = if scrollbar_info.needs_horizontal {
2558 Self::calculate_scrollbar_opacity(
2559 scroll_manager.get_last_activity_time(dom_id, node_id),
2560 now.clone(),
2561 fade_delay,
2562 fade_duration,
2563 )
2564 } else {
2565 0.0
2566 };
2567
2568 if scrollbar_info.needs_vertical && vertical_opacity > 0.001 {
2570 let key = (dom_id, node_id);
2571 let existing = gpu_cache.scrollbar_v_opacity_values.get(&key);
2572
2573 match existing {
2574 None => {
2575 let opacity_key = OpacityKey::unique();
2576 gpu_cache.scrollbar_v_opacity_keys.insert(key, opacity_key);
2577 gpu_cache
2578 .scrollbar_v_opacity_values
2579 .insert(key, vertical_opacity);
2580 events.push(GpuScrollbarOpacityEvent::VerticalAdded(
2581 dom_id,
2582 node_id,
2583 opacity_key,
2584 vertical_opacity,
2585 ));
2586 }
2587 Some(&old_opacity) if (old_opacity - vertical_opacity).abs() > 0.001 => {
2588 let opacity_key = gpu_cache.scrollbar_v_opacity_keys[&key];
2589 gpu_cache
2590 .scrollbar_v_opacity_values
2591 .insert(key, vertical_opacity);
2592 events.push(GpuScrollbarOpacityEvent::VerticalChanged(
2593 dom_id,
2594 node_id,
2595 opacity_key,
2596 old_opacity,
2597 vertical_opacity,
2598 ));
2599 }
2600 _ => {}
2601 }
2602 } else {
2603 let key = (dom_id, node_id);
2605 if let Some(opacity_key) = gpu_cache.scrollbar_v_opacity_keys.remove(&key) {
2606 gpu_cache.scrollbar_v_opacity_values.remove(&key);
2607 events.push(GpuScrollbarOpacityEvent::VerticalRemoved(
2608 dom_id,
2609 node_id,
2610 opacity_key,
2611 ));
2612 }
2613 }
2614
2615 if scrollbar_info.needs_horizontal && horizontal_opacity > 0.001 {
2617 let key = (dom_id, node_id);
2618 let existing = gpu_cache.scrollbar_h_opacity_values.get(&key);
2619
2620 match existing {
2621 None => {
2622 let opacity_key = OpacityKey::unique();
2623 gpu_cache.scrollbar_h_opacity_keys.insert(key, opacity_key);
2624 gpu_cache
2625 .scrollbar_h_opacity_values
2626 .insert(key, horizontal_opacity);
2627 events.push(GpuScrollbarOpacityEvent::HorizontalAdded(
2628 dom_id,
2629 node_id,
2630 opacity_key,
2631 horizontal_opacity,
2632 ));
2633 }
2634 Some(&old_opacity) if (old_opacity - horizontal_opacity).abs() > 0.001 => {
2635 let opacity_key = gpu_cache.scrollbar_h_opacity_keys[&key];
2636 gpu_cache
2637 .scrollbar_h_opacity_values
2638 .insert(key, horizontal_opacity);
2639 events.push(GpuScrollbarOpacityEvent::HorizontalChanged(
2640 dom_id,
2641 node_id,
2642 opacity_key,
2643 old_opacity,
2644 horizontal_opacity,
2645 ));
2646 }
2647 _ => {}
2648 }
2649 } else {
2650 let key = (dom_id, node_id);
2652 if let Some(opacity_key) = gpu_cache.scrollbar_h_opacity_keys.remove(&key) {
2653 gpu_cache.scrollbar_h_opacity_values.remove(&key);
2654 events.push(GpuScrollbarOpacityEvent::HorizontalRemoved(
2655 dom_id,
2656 node_id,
2657 opacity_key,
2658 ));
2659 }
2660 }
2661 }
2662
2663 events
2664 }
2665
2666 pub fn compute_scroll_ids(
2675 layout_tree: &LayoutTree,
2676 styled_dom: &azul_core::styled_dom::StyledDom,
2677 ) -> (BTreeMap<usize, u64>, BTreeMap<u64, NodeId>) {
2678 use azul_css::props::layout::LayoutOverflow;
2679
2680 use crate::solver3::getters::{get_overflow_x, get_overflow_y};
2681
2682 let mut scroll_ids = BTreeMap::new();
2683 let mut scroll_id_to_node_id = BTreeMap::new();
2684
2685 for (layout_idx, node) in layout_tree.nodes.iter().enumerate() {
2687 let Some(dom_node_id) = node.dom_node_id else {
2688 continue;
2689 };
2690
2691 let styled_node_state = styled_dom
2693 .styled_nodes
2694 .as_container()
2695 .get(dom_node_id)
2696 .map(|n| n.styled_node_state.clone())
2697 .unwrap_or_default();
2698
2699 let overflow_x = get_overflow_x(styled_dom, dom_node_id, &styled_node_state);
2701 let overflow_y = get_overflow_y(styled_dom, dom_node_id, &styled_node_state);
2702
2703 let is_scrollable = overflow_x.is_scroll() || overflow_y.is_scroll();
2704
2705 if !is_scrollable {
2706 continue;
2707 }
2708
2709 let scroll_id = node.node_data_hash;
2712
2713 scroll_ids.insert(layout_idx, scroll_id);
2714 scroll_id_to_node_id.insert(scroll_id, dom_node_id);
2715 }
2716
2717 (scroll_ids, scroll_id_to_node_id)
2718 }
2719
2720 pub fn get_node_layout_rect(
2727 &self,
2728 node_id: azul_core::dom::DomNodeId,
2729 ) -> Option<azul_core::geom::LogicalRect> {
2730 let layout_tree = self.layout_cache.tree.as_ref()?;
2732
2733 let target_node_id = node_id.node.into_crate_internal();
2736 let layout_idx = layout_tree
2737 .nodes
2738 .iter()
2739 .position(|node| node.dom_node_id == target_node_id)?;
2740
2741 let calc_pos = self.layout_cache.calculated_positions.get(&layout_idx)?;
2743
2744 let layout_node = layout_tree.nodes.get(layout_idx)?;
2746
2747 let used_size = layout_node.used_size?;
2749
2750 let hidpi_factor = self
2752 .current_window_state
2753 .size
2754 .get_hidpi_factor()
2755 .inner
2756 .get();
2757
2758 Some(LogicalRect::new(
2759 LogicalPosition::new(calc_pos.x as f32, calc_pos.y as f32),
2760 LogicalSize::new(
2761 used_size.width / hidpi_factor,
2762 used_size.height / hidpi_factor,
2763 ),
2764 ))
2765 }
2766
2767 pub fn get_focused_cursor_rect(&self) -> Option<azul_core::geom::LogicalRect> {
2782 let focused_node = self.focus_manager.focused_node?;
2784
2785 let cursor = self.cursor_manager.get_cursor()?;
2787
2788 let layout_tree = self.layout_cache.tree.as_ref()?;
2790
2791 let target_node_id = focused_node.node.into_crate_internal();
2793 let layout_idx = layout_tree
2794 .nodes
2795 .iter()
2796 .position(|node| node.dom_node_id == target_node_id)?;
2797
2798 let layout_node = layout_tree.nodes.get(layout_idx)?;
2800
2801 let cached_layout = layout_node.inline_layout_result.as_ref()?;
2803 let inline_layout = &cached_layout.layout;
2804
2805 let mut cursor_rect = inline_layout.get_cursor_rect(cursor)?;
2807
2808 let calc_pos = self.layout_cache.calculated_positions.get(&layout_idx)?;
2810
2811 cursor_rect.origin.x += calc_pos.x as f32;
2813 cursor_rect.origin.y += calc_pos.y as f32;
2814
2815 Some(cursor_rect)
2817 }
2818
2819 pub fn get_focused_cursor_rect_viewport(&self) -> Option<azul_core::geom::LogicalRect> {
2837 let mut cursor_rect = self.get_focused_cursor_rect()?;
2839
2840 let focused_node = self.focus_manager.focused_node?;
2842
2843 let layout_tree = self.layout_cache.tree.as_ref()?;
2845
2846 let target_node_id = focused_node.node.into_crate_internal();
2848 let layout_idx = layout_tree
2849 .nodes
2850 .iter()
2851 .position(|node| node.dom_node_id == target_node_id)?;
2852
2853 let gpu_cache = self.gpu_state_manager.caches.get(&focused_node.dom);
2855
2856 let mut current_layout_idx = layout_idx;
2860
2861 while let Some(parent_idx) = layout_tree.nodes.get(current_layout_idx)?.parent {
2862 if let Some(parent_dom_node_id) = layout_tree.nodes.get(parent_idx)?.dom_node_id {
2864 if let Some(scroll_state) = self
2866 .scroll_manager
2867 .get_scroll_state(focused_node.dom, parent_dom_node_id)
2868 {
2869 cursor_rect.origin.x -= scroll_state.current_offset.x;
2871 cursor_rect.origin.y -= scroll_state.current_offset.y;
2872 }
2873
2874 if let Some(cache) = gpu_cache {
2876 if let Some(transform) = cache.current_transform_values.get(&parent_dom_node_id)
2877 {
2878 let inverse = transform.inverse();
2881 if let Some(transformed_origin) =
2882 inverse.transform_point2d(cursor_rect.origin)
2883 {
2884 cursor_rect.origin = transformed_origin;
2885 }
2886 }
2888 }
2889 }
2890
2891 current_layout_idx = parent_idx;
2893 }
2894
2895 Some(cursor_rect)
2896 }
2897
2898 pub fn find_scrollable_ancestor(
2902 &self,
2903 mut node_id: azul_core::dom::DomNodeId,
2904 ) -> Option<azul_core::dom::DomNodeId> {
2905 let layout_tree = self.layout_cache.tree.as_ref()?;
2907
2908 let mut current_node_id = node_id.node.into_crate_internal();
2910
2911 loop {
2913 let layout_idx = layout_tree
2915 .nodes
2916 .iter()
2917 .position(|node| node.dom_node_id == current_node_id)?;
2918
2919 let layout_node = layout_tree.nodes.get(layout_idx)?;
2920
2921 if layout_node.scrollbar_info.is_some() {
2923 let check_node_id = current_node_id?;
2925 if self
2926 .scroll_manager
2927 .get_scroll_state(node_id.dom, check_node_id)
2928 .is_some()
2929 {
2930 return Some(azul_core::dom::DomNodeId {
2932 dom: node_id.dom,
2933 node: azul_core::styled_dom::NodeHierarchyItemId::from_crate_internal(
2934 Some(check_node_id),
2935 ),
2936 });
2937 }
2938 }
2939
2940 let parent_idx = layout_node.parent?;
2942 let parent_node = layout_tree.nodes.get(parent_idx)?;
2943 current_node_id = parent_node.dom_node_id;
2944 }
2945 }
2946
2947 pub fn scroll_selection_into_view(
2973 &mut self,
2974 scroll_type: SelectionScrollType,
2975 scroll_mode: ScrollMode,
2976 ) -> bool {
2977 let bounds = match scroll_type {
2979 SelectionScrollType::Cursor => {
2980 match self.get_focused_cursor_rect() {
2982 Some(rect) => rect,
2983 None => return false, }
2985 }
2986 SelectionScrollType::Selection => {
2987 match self.get_focused_cursor_rect() {
2990 Some(rect) => rect,
2991 None => return false, }
2993 }
3000 SelectionScrollType::DragSelection { mouse_position } => {
3001 LogicalRect::new(mouse_position, LogicalSize::zero())
3003 }
3004 };
3005
3006 let focused_node = match self.focus_manager.focused_node {
3008 Some(node) => node,
3009 None => return false,
3010 };
3011
3012 let scroll_container = match self.find_scrollable_ancestor(focused_node) {
3014 Some(node) => node,
3015 None => return false, };
3017
3018 let layout_tree = match self.layout_cache.tree.as_ref() {
3020 Some(tree) => tree,
3021 None => return false,
3022 };
3023
3024 let scrollable_node_internal = match scroll_container.node.into_crate_internal() {
3025 Some(id) => id,
3026 None => return false,
3027 };
3028
3029 let layout_idx = match layout_tree
3030 .nodes
3031 .iter()
3032 .position(|n| n.dom_node_id == Some(scrollable_node_internal))
3033 {
3034 Some(idx) => idx,
3035 None => return false,
3036 };
3037
3038 let scrollable_layout_node = match layout_tree.nodes.get(layout_idx) {
3039 Some(node) => node,
3040 None => return false,
3041 };
3042
3043 let container_pos = self
3044 .layout_cache
3045 .calculated_positions
3046 .get(&layout_idx)
3047 .copied()
3048 .unwrap_or_default();
3049
3050 let container_size = scrollable_layout_node.used_size.unwrap_or_default();
3051
3052 let container_rect = LogicalRect {
3053 origin: container_pos,
3054 size: container_size,
3055 };
3056
3057 let scroll_state = match self
3059 .scroll_manager
3060 .get_scroll_state(scroll_container.dom, scrollable_node_internal)
3061 {
3062 Some(state) => state,
3063 None => return false,
3064 };
3065
3066 let visible_area = LogicalRect::new(
3068 LogicalPosition::new(
3069 container_rect.origin.x + scroll_state.current_offset.x,
3070 container_rect.origin.y + scroll_state.current_offset.y,
3071 ),
3072 container_rect.size,
3073 );
3074
3075 let scroll_delta = match scroll_mode {
3077 ScrollMode::Instant => {
3078 calculate_instant_scroll_delta(bounds, visible_area)
3080 }
3081 ScrollMode::Accelerated => {
3082 let distance = calculate_edge_distance(bounds, visible_area);
3084 calculate_accelerated_scroll_delta(distance)
3085 }
3086 };
3087
3088 if scroll_delta.x != 0.0 || scroll_delta.y != 0.0 {
3090 let duration = match scroll_mode {
3091 ScrollMode::Instant => Duration::System(SystemTimeDiff { secs: 0, nanos: 0 }),
3092 ScrollMode::Accelerated => Duration::System(SystemTimeDiff {
3093 secs: 0,
3094 nanos: 16_666_667,
3095 }), };
3097
3098 let external = ExternalSystemCallbacks::rust_internal();
3099 let now = (external.get_system_time_fn.cb)();
3100
3101 let new_target = LogicalPosition {
3103 x: scroll_state.current_offset.x + scroll_delta.x,
3104 y: scroll_state.current_offset.y + scroll_delta.y,
3105 };
3106
3107 self.scroll_manager.scroll_to(
3108 scroll_container.dom,
3109 scrollable_node_internal,
3110 new_target,
3111 duration,
3112 EasingFunction::Linear,
3113 now.into(),
3114 );
3115
3116 true } else {
3118 false }
3120 }
3121
3122 fn scroll_focused_cursor_into_view(&mut self) {
3137 self.scroll_selection_into_view(SelectionScrollType::Cursor, ScrollMode::Instant);
3139 }
3140}
3141
3142#[derive(Debug, Clone, Copy)]
3144pub enum SelectionScrollType {
3145 Cursor,
3147 Selection,
3149 DragSelection { mouse_position: LogicalPosition },
3151}
3152
3153#[derive(Debug, Clone, Copy)]
3155pub enum ScrollMode {
3156 Instant,
3158 Accelerated,
3160}
3161
3162#[derive(Debug, Clone, Copy)]
3164struct EdgeDistance {
3165 left: f32,
3166 right: f32,
3167 top: f32,
3168 bottom: f32,
3169}
3170
3171fn calculate_edge_distance(rect: LogicalRect, container: LogicalRect) -> EdgeDistance {
3173 EdgeDistance {
3174 left: (rect.origin.x - container.origin.x).max(0.0),
3176 right: ((container.origin.x + container.size.width) - (rect.origin.x + rect.size.width))
3178 .max(0.0),
3179 top: (rect.origin.y - container.origin.y).max(0.0),
3181 bottom: ((container.origin.y + container.size.height) - (rect.origin.y + rect.size.height))
3183 .max(0.0),
3184 }
3185}
3186
3187fn calculate_instant_scroll_delta(
3189 bounds: LogicalRect,
3190 visible_area: LogicalRect,
3191) -> LogicalPosition {
3192 const PADDING: f32 = 5.0;
3193 let mut delta = LogicalPosition::zero();
3194
3195 if bounds.origin.x < visible_area.origin.x + PADDING {
3197 delta.x = bounds.origin.x - visible_area.origin.x - PADDING;
3198 } else if bounds.origin.x + bounds.size.width
3199 > visible_area.origin.x + visible_area.size.width - PADDING
3200 {
3201 delta.x = (bounds.origin.x + bounds.size.width)
3202 - (visible_area.origin.x + visible_area.size.width)
3203 + PADDING;
3204 }
3205
3206 if bounds.origin.y < visible_area.origin.y + PADDING {
3208 delta.y = bounds.origin.y - visible_area.origin.y - PADDING;
3209 } else if bounds.origin.y + bounds.size.height
3210 > visible_area.origin.y + visible_area.size.height - PADDING
3211 {
3212 delta.y = (bounds.origin.y + bounds.size.height)
3213 - (visible_area.origin.y + visible_area.size.height)
3214 + PADDING;
3215 }
3216
3217 delta
3218}
3219
3220fn calculate_accelerated_scroll_delta(distance: EdgeDistance) -> LogicalPosition {
3222 const DEAD_ZONE: f32 = 20.0;
3224 const SLOW_ZONE: f32 = 50.0;
3225 const MEDIUM_ZONE: f32 = 100.0;
3226 const FAST_ZONE: f32 = 200.0;
3227
3228 const SLOW_SPEED: f32 = 2.0;
3230 const MEDIUM_SPEED: f32 = 4.0;
3231 const FAST_SPEED: f32 = 8.0;
3232 const VERY_FAST_SPEED: f32 = 16.0;
3233
3234 let speed_for_distance = |dist: f32| -> f32 {
3236 if dist < DEAD_ZONE {
3237 0.0
3238 } else if dist < SLOW_ZONE {
3239 SLOW_SPEED
3240 } else if dist < MEDIUM_ZONE {
3241 MEDIUM_SPEED
3242 } else if dist < FAST_ZONE {
3243 FAST_SPEED
3244 } else {
3245 VERY_FAST_SPEED
3246 }
3247 };
3248
3249 let scroll_x = if distance.left < distance.right {
3251 -speed_for_distance(distance.left)
3253 } else {
3254 speed_for_distance(distance.right)
3256 };
3257
3258 let scroll_y = if distance.top < distance.bottom {
3260 -speed_for_distance(distance.top)
3262 } else {
3263 speed_for_distance(distance.bottom)
3265 };
3266
3267 LogicalPosition::new(scroll_x, scroll_y)
3268}
3269
3270pub struct LayoutResult {
3272 pub display_list: DisplayList,
3273 pub warnings: Vec<String>,
3274}
3275
3276impl LayoutResult {
3277 pub fn new(display_list: DisplayList, warnings: Vec<String>) -> Self {
3278 Self {
3279 display_list,
3280 warnings,
3281 }
3282 }
3283}
3284
3285impl LayoutWindow {
3286 #[cfg(feature = "std")]
3291 pub fn run_single_timer(
3292 &mut self,
3293 timer_id: usize,
3294 frame_start: Instant,
3295 current_window_handle: &RawWindowHandle,
3296 gl_context: &OptionGlContextPtr,
3297 image_cache: &mut ImageCache,
3298 system_fonts: &mut FcFontCache,
3299 system_style: std::sync::Arc<azul_css::system::SystemStyle>,
3300 system_callbacks: &ExternalSystemCallbacks,
3301 previous_window_state: &Option<FullWindowState>,
3302 current_window_state: &FullWindowState,
3303 renderer_resources: &RendererResources,
3304 ) -> CallCallbacksResult {
3305 use std::collections::BTreeMap;
3306
3307 use crate::callbacks::{CallCallbacksResult, CallbackInfo};
3308
3309 let mut ret = CallCallbacksResult {
3310 should_scroll_render: false,
3311 callbacks_update_screen: Update::DoNothing,
3312 modified_window_state: None,
3313 css_properties_changed: None,
3314 words_changed: None,
3315 images_changed: None,
3316 image_masks_changed: None,
3317 image_callbacks_changed: None,
3318 nodes_scrolled_in_callbacks: None,
3319 update_focused_node: FocusUpdateRequest::NoChange,
3320 timers: None,
3321 threads: None,
3322 timers_removed: None,
3323 threads_removed: None,
3324 windows_created: Vec::new(),
3325 menus_to_open: Vec::new(),
3326 tooltips_to_show: Vec::new(),
3327 hide_tooltip: false,
3328 cursor_changed: false,
3329 stop_propagation: false,
3330 prevent_default: false,
3331 hit_test_update_requested: None,
3332 queued_window_states: Vec::new(),
3333 text_input_triggered: Vec::new(),
3334 };
3335
3336 let mut should_terminate = TerminateTimer::Continue;
3337
3338 let current_scroll_states_nested = self.get_nested_scroll_states(DomId::ROOT_ID);
3339
3340 let timer_exists = self.timers.contains_key(&TimerId { id: timer_id });
3342 let timer_node_id = self
3343 .timers
3344 .get(&TimerId { id: timer_id })
3345 .and_then(|t| t.node_id.into_option());
3346
3347 if timer_exists {
3348 let hit_dom_node = match timer_node_id {
3350 Some(s) => s,
3351 None => DomNodeId {
3352 dom: DomId::ROOT_ID,
3353 node: NodeHierarchyItemId::from_crate_internal(None),
3354 },
3355 };
3356 let cursor_relative_to_item = OptionLogicalPosition::None;
3357 let cursor_in_viewport = OptionLogicalPosition::None;
3358
3359 let callback_changes = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
3363
3364 let timer_ctx = self
3367 .timers
3368 .get(&TimerId { id: timer_id })
3369 .map(|t| t.callback.ctx.clone())
3370 .unwrap_or(OptionRefAny::None);
3371
3372 let ref_data = crate::callbacks::CallbackInfoRefData {
3373 layout_window: self,
3374 renderer_resources,
3375 previous_window_state,
3376 current_window_state,
3377 gl_context,
3378 current_scroll_manager: ¤t_scroll_states_nested,
3379 current_window_handle,
3380 system_callbacks,
3381 system_style,
3382 #[cfg(feature = "icu")]
3383 icu_localizer: self.icu_localizer.clone(),
3384 ctx: timer_ctx,
3385 };
3386
3387 let callback_info = CallbackInfo::new(
3388 &ref_data,
3389 &callback_changes,
3390 hit_dom_node,
3391 cursor_relative_to_item,
3392 cursor_in_viewport,
3393 ); let timer = self.timers.get_mut(&TimerId { id: timer_id }).unwrap();
3395 let tcr = timer.invoke(&callback_info, &system_callbacks.get_system_time_fn);
3396
3397 ret.callbacks_update_screen = tcr.should_update;
3398 should_terminate = tcr.should_terminate;
3399
3400 let collected_changes = callback_changes
3403 .lock()
3404 .map(|mut guard| core::mem::take(&mut *guard))
3405 .unwrap_or_default();
3406
3407 let change_result = self.apply_callback_changes(
3409 collected_changes,
3410 current_window_state,
3411 image_cache,
3412 system_fonts,
3413 );
3414
3415 if !change_result.iframes_to_update.is_empty() {
3417 self.queue_iframe_updates(change_result.iframes_to_update.clone());
3418 }
3419
3420 ret.stop_propagation = change_result.stop_propagation;
3422 ret.prevent_default = change_result.prevent_default;
3423 ret.tooltips_to_show = change_result.tooltips_to_show;
3424 ret.hide_tooltip = change_result.hide_tooltip;
3425
3426 if !change_result.timers.is_empty() {
3427 ret.timers = Some(change_result.timers);
3428 }
3429 if !change_result.threads.is_empty() {
3430 ret.threads = Some(change_result.threads);
3431 }
3432 if change_result.modified_window_state != *current_window_state {
3433 ret.modified_window_state = Some(change_result.modified_window_state);
3434 }
3435 if !change_result.threads_removed.is_empty() {
3436 ret.threads_removed = Some(change_result.threads_removed);
3437 }
3438 if !change_result.timers_removed.is_empty() {
3439 ret.timers_removed = Some(change_result.timers_removed);
3440 }
3441 if !change_result.words_changed.is_empty() {
3442 ret.words_changed = Some(change_result.words_changed);
3443 }
3444 if !change_result.images_changed.is_empty() {
3445 ret.images_changed = Some(change_result.images_changed);
3446 }
3447 if !change_result.image_masks_changed.is_empty() {
3448 ret.image_masks_changed = Some(change_result.image_masks_changed);
3449 }
3450 if !change_result.css_properties_changed.is_empty() {
3451 ret.css_properties_changed = Some(change_result.css_properties_changed);
3452 }
3453 if !change_result.image_callbacks_changed.is_empty() {
3454 ret.image_callbacks_changed = Some(change_result.image_callbacks_changed);
3455 }
3456 if !change_result.nodes_scrolled.is_empty() {
3457 ret.nodes_scrolled_in_callbacks = Some(change_result.nodes_scrolled);
3458 }
3459
3460 if change_result.hit_test_update_requested.is_some() {
3462 ret.hit_test_update_requested = change_result.hit_test_update_requested;
3463 }
3464
3465 if !change_result.queued_window_states.is_empty() {
3467 ret.queued_window_states = change_result.queued_window_states;
3468 }
3469
3470 if !change_result.text_input_triggered.is_empty() {
3472 println!("[run_single_timer] Forwarding {} text_input_triggered events", change_result.text_input_triggered.len());
3473 ret.text_input_triggered = change_result.text_input_triggered;
3474 }
3475
3476 if let Some(ft) = change_result.focus_target {
3478 if let Ok(new_focus_node) = crate::managers::focus_cursor::resolve_focus_target(
3479 &ft,
3480 &self.layout_results,
3481 self.focus_manager.get_focused_node().copied(),
3482 ) {
3483 ret.update_focused_node = match new_focus_node {
3484 Some(node) => FocusUpdateRequest::FocusNode(node),
3485 None => FocusUpdateRequest::ClearFocus,
3486 };
3487 }
3488 }
3489 }
3490
3491 if should_terminate == TerminateTimer::Terminate {
3492 ret.timers_removed
3493 .get_or_insert_with(|| std::collections::BTreeSet::new())
3494 .insert(TimerId { id: timer_id });
3495 }
3496
3497 return ret;
3498 }
3499
3500 #[cfg(feature = "std")]
3501 pub fn run_all_threads(
3502 &mut self,
3503 data: &mut RefAny,
3504 current_window_handle: &RawWindowHandle,
3505 gl_context: &OptionGlContextPtr,
3506 image_cache: &mut ImageCache,
3507 system_fonts: &mut FcFontCache,
3508 system_style: std::sync::Arc<azul_css::system::SystemStyle>,
3509 system_callbacks: &ExternalSystemCallbacks,
3510 previous_window_state: &Option<FullWindowState>,
3511 current_window_state: &FullWindowState,
3512 renderer_resources: &RendererResources,
3513 ) -> CallCallbacksResult {
3514 use std::collections::BTreeSet;
3515
3516 use crate::{
3517 callbacks::{CallCallbacksResult, CallbackInfo},
3518 thread::{OptionThreadReceiveMsg, ThreadReceiveMsg, ThreadWriteBackMsg},
3519 };
3520
3521 let mut ret = CallCallbacksResult {
3522 should_scroll_render: false,
3523 callbacks_update_screen: Update::DoNothing,
3524 modified_window_state: None,
3525 css_properties_changed: None,
3526 words_changed: None,
3527 images_changed: None,
3528 image_masks_changed: None,
3529 image_callbacks_changed: None,
3530 nodes_scrolled_in_callbacks: None,
3531 update_focused_node: FocusUpdateRequest::NoChange,
3532 timers: None,
3533 threads: None,
3534 timers_removed: None,
3535 threads_removed: None,
3536 windows_created: Vec::new(),
3537 menus_to_open: Vec::new(),
3538 tooltips_to_show: Vec::new(),
3539 hide_tooltip: false,
3540 cursor_changed: false,
3541 stop_propagation: false,
3542 prevent_default: false,
3543 hit_test_update_requested: None,
3544 queued_window_states: Vec::new(),
3545 text_input_triggered: Vec::new(),
3546 };
3547
3548 let mut ret_modified_window_state = current_window_state.clone();
3549 let ret_window_state = ret_modified_window_state.clone();
3550 let mut ret_timers = FastHashMap::new();
3551 let mut ret_timers_removed = FastBTreeSet::new();
3552 let mut ret_threads = FastHashMap::new();
3553 let mut ret_threads_removed = FastBTreeSet::new();
3554 let mut ret_words_changed = BTreeMap::new();
3555 let mut ret_images_changed = BTreeMap::new();
3556 let mut ret_image_masks_changed = BTreeMap::new();
3557 let mut ret_css_properties_changed = BTreeMap::new();
3558 let mut ret_nodes_scrolled_in_callbacks = BTreeMap::new();
3559 let mut new_focus_target = None;
3560 let mut stop_propagation = false;
3561 let current_scroll_states = self.get_nested_scroll_states(DomId::ROOT_ID);
3562
3563 let thread_ids: Vec<ThreadId> = self.threads.keys().copied().collect();
3565
3566 for thread_id in thread_ids {
3567 let thread = match self.threads.get_mut(&thread_id) {
3568 Some(t) => t,
3569 None => continue,
3570 };
3571
3572 let hit_dom_node = DomNodeId {
3573 dom: DomId::ROOT_ID,
3574 node: NodeHierarchyItemId::from_crate_internal(None),
3575 };
3576 let cursor_relative_to_item = OptionLogicalPosition::None;
3577 let cursor_in_viewport = OptionLogicalPosition::None;
3578
3579 let (msg, writeback_data_ptr, is_finished) = {
3581 let thread_inner = &mut *match thread.ptr.lock().ok() {
3582 Some(s) => s,
3583 None => {
3584 ret.threads_removed
3585 .get_or_insert_with(|| BTreeSet::default())
3586 .insert(thread_id);
3587 continue;
3588 }
3589 };
3590
3591 let _ = thread_inner.sender_send(ThreadSendMsg::Tick);
3592 let update = thread_inner.receiver_try_recv();
3593 let msg = match update {
3594 OptionThreadReceiveMsg::None => continue,
3595 OptionThreadReceiveMsg::Some(s) => s,
3596 };
3597
3598 let writeback_data_ptr: *mut RefAny = &mut thread_inner.writeback_data as *mut _;
3599 let is_finished = thread_inner.is_finished();
3600
3601 (msg, writeback_data_ptr, is_finished)
3602 };
3604
3605 let ThreadWriteBackMsg {
3606 refany: mut data,
3607 callback,
3608 } = match msg {
3609 ThreadReceiveMsg::Update(update_screen) => {
3610 ret.callbacks_update_screen.max_self(update_screen);
3611 continue;
3612 }
3613 ThreadReceiveMsg::WriteBack(t) => t,
3614 };
3615
3616 let callback_changes = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
3618
3619 let ref_data = crate::callbacks::CallbackInfoRefData {
3621 layout_window: self,
3622 renderer_resources,
3623 previous_window_state,
3624 current_window_state,
3625 gl_context,
3626 current_scroll_manager: ¤t_scroll_states,
3627 current_window_handle,
3628 system_callbacks,
3629 system_style: system_style.clone(),
3630 #[cfg(feature = "icu")]
3631 icu_localizer: self.icu_localizer.clone(),
3632 ctx: callback.ctx.clone(),
3633 };
3634
3635 let callback_info = CallbackInfo::new(
3636 &ref_data,
3637 &callback_changes,
3638 hit_dom_node,
3639 cursor_relative_to_item,
3640 cursor_in_viewport,
3641 );
3642 let callback_update = (callback.cb)(
3643 unsafe { (*writeback_data_ptr).clone() },
3644 data.clone(),
3645 callback_info,
3646 );
3647 ret.callbacks_update_screen.max_self(callback_update);
3648
3649 let collected_changes = callback_changes
3651 .lock()
3652 .map(|mut guard| core::mem::take(&mut *guard))
3653 .unwrap_or_default();
3654
3655 let change_result = self.apply_callback_changes(
3657 collected_changes,
3658 current_window_state,
3659 image_cache,
3660 system_fonts,
3661 );
3662
3663 self.queue_iframe_updates(change_result.iframes_to_update);
3665
3666 ret.stop_propagation = ret.stop_propagation || change_result.stop_propagation;
3667 ret.prevent_default = ret.prevent_default || change_result.prevent_default;
3668 ret.tooltips_to_show.extend(change_result.tooltips_to_show);
3669 ret.hide_tooltip = ret.hide_tooltip || change_result.hide_tooltip;
3670
3671 if change_result.hit_test_update_requested.is_some() {
3673 ret.hit_test_update_requested = change_result.hit_test_update_requested;
3674 }
3675
3676 ret_timers.extend(change_result.timers);
3678 ret_threads.extend(change_result.threads);
3679 ret_timers_removed.extend(change_result.timers_removed);
3680 ret_threads_removed.extend(change_result.threads_removed);
3681
3682 for (dom_id, nodes) in change_result.words_changed {
3683 ret_words_changed
3684 .entry(dom_id)
3685 .or_insert_with(BTreeMap::new)
3686 .extend(nodes);
3687 }
3688 for (dom_id, nodes) in change_result.images_changed {
3689 ret_images_changed
3690 .entry(dom_id)
3691 .or_insert_with(BTreeMap::new)
3692 .extend(nodes);
3693 }
3694 for (dom_id, nodes) in change_result.image_masks_changed {
3695 ret_image_masks_changed
3696 .entry(dom_id)
3697 .or_insert_with(BTreeMap::new)
3698 .extend(nodes);
3699 }
3700 for (dom_id, nodes) in change_result.css_properties_changed {
3701 ret_css_properties_changed
3702 .entry(dom_id)
3703 .or_insert_with(BTreeMap::new)
3704 .extend(nodes);
3705 }
3706 for (dom_id, nodes) in change_result.nodes_scrolled {
3707 ret_nodes_scrolled_in_callbacks
3708 .entry(dom_id)
3709 .or_insert_with(BTreeMap::new)
3710 .extend(nodes);
3711 }
3712
3713 if change_result.modified_window_state != *current_window_state {
3714 ret_modified_window_state = change_result.modified_window_state;
3715 }
3716
3717 if let Some(ft) = change_result.focus_target {
3718 new_focus_target = Some(ft);
3719 }
3720
3721 if is_finished {
3722 ret.threads_removed
3723 .get_or_insert_with(|| BTreeSet::default())
3724 .insert(thread_id);
3725 }
3726 }
3727
3728 if !ret_timers.is_empty() {
3729 ret.timers = Some(ret_timers);
3730 }
3731 if !ret_threads.is_empty() {
3732 ret.threads = Some(ret_threads);
3733 }
3734 if ret_modified_window_state != ret_window_state {
3735 ret.modified_window_state = Some(ret_modified_window_state);
3736 }
3737 if !ret_threads_removed.is_empty() {
3738 ret.threads_removed = Some(ret_threads_removed);
3739 }
3740 if !ret_timers_removed.is_empty() {
3741 ret.timers_removed = Some(ret_timers_removed);
3742 }
3743 if !ret_words_changed.is_empty() {
3744 ret.words_changed = Some(ret_words_changed);
3745 }
3746 if !ret_images_changed.is_empty() {
3747 ret.images_changed = Some(ret_images_changed);
3748 }
3749 if !ret_image_masks_changed.is_empty() {
3750 ret.image_masks_changed = Some(ret_image_masks_changed);
3751 }
3752 if !ret_css_properties_changed.is_empty() {
3753 ret.css_properties_changed = Some(ret_css_properties_changed);
3754 }
3755 if !ret_nodes_scrolled_in_callbacks.is_empty() {
3756 ret.nodes_scrolled_in_callbacks = Some(ret_nodes_scrolled_in_callbacks);
3757 }
3758
3759 if let Some(ft) = new_focus_target {
3760 if let Ok(new_focus_node) = crate::managers::focus_cursor::resolve_focus_target(
3761 &ft,
3762 &self.layout_results,
3763 self.focus_manager.get_focused_node().copied(),
3764 ) {
3765 ret.update_focused_node = match new_focus_node {
3766 Some(node) => FocusUpdateRequest::FocusNode(node),
3767 None => FocusUpdateRequest::ClearFocus,
3768 };
3769 }
3770 }
3771
3772 return ret;
3773 }
3774
3775 pub fn invoke_single_callback(
3777 &mut self,
3778 callback: &mut Callback,
3779 data: &mut RefAny,
3780 current_window_handle: &RawWindowHandle,
3781 gl_context: &OptionGlContextPtr,
3782 image_cache: &mut ImageCache,
3783 system_fonts: &mut FcFontCache,
3784 system_style: std::sync::Arc<azul_css::system::SystemStyle>,
3785 system_callbacks: &ExternalSystemCallbacks,
3786 previous_window_state: &Option<FullWindowState>,
3787 current_window_state: &FullWindowState,
3788 renderer_resources: &RendererResources,
3789 ) -> CallCallbacksResult {
3790 use crate::callbacks::{CallCallbacksResult, Callback, CallbackInfo};
3791
3792 let hit_dom_node = DomNodeId {
3793 dom: DomId::ROOT_ID,
3794 node: NodeHierarchyItemId::from_crate_internal(None),
3795 };
3796
3797 let mut ret = CallCallbacksResult {
3798 should_scroll_render: false,
3799 callbacks_update_screen: Update::DoNothing,
3800 modified_window_state: None,
3801 css_properties_changed: None,
3802 words_changed: None,
3803 images_changed: None,
3804 image_masks_changed: None,
3805 image_callbacks_changed: None,
3806 nodes_scrolled_in_callbacks: None,
3807 update_focused_node: FocusUpdateRequest::NoChange,
3808 timers: None,
3809 threads: None,
3810 timers_removed: None,
3811 threads_removed: None,
3812 windows_created: Vec::new(),
3813 menus_to_open: Vec::new(),
3814 tooltips_to_show: Vec::new(),
3815 hide_tooltip: false,
3816 cursor_changed: false,
3817 stop_propagation: false,
3818 prevent_default: false,
3819 hit_test_update_requested: None,
3820 queued_window_states: Vec::new(),
3821 text_input_triggered: Vec::new(),
3822 };
3823
3824 let mut ret_modified_window_state = current_window_state.clone();
3825 let ret_window_state = ret_modified_window_state.clone();
3826 let mut ret_timers = FastHashMap::new();
3827 let mut ret_timers_removed = FastBTreeSet::new();
3828 let mut ret_threads = FastHashMap::new();
3829 let mut ret_threads_removed = FastBTreeSet::new();
3830 let mut ret_words_changed = BTreeMap::new();
3831 let mut ret_images_changed = BTreeMap::new();
3832 let mut ret_image_masks_changed = BTreeMap::new();
3833 let mut ret_css_properties_changed = BTreeMap::new();
3834 let mut ret_nodes_scrolled_in_callbacks = BTreeMap::new();
3835 let mut new_focus_target = None;
3836 let mut stop_propagation = false;
3837 let current_scroll_states = self.get_nested_scroll_states(DomId::ROOT_ID);
3838
3839 let cursor_relative_to_item = OptionLogicalPosition::None;
3840 let cursor_in_viewport = OptionLogicalPosition::None;
3841
3842 let callback_changes = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
3844
3845 let ref_data = crate::callbacks::CallbackInfoRefData {
3847 layout_window: self,
3848 renderer_resources,
3849 previous_window_state,
3850 current_window_state,
3851 gl_context,
3852 current_scroll_manager: ¤t_scroll_states,
3853 current_window_handle,
3854 system_callbacks,
3855 system_style,
3856 #[cfg(feature = "icu")]
3857 icu_localizer: self.icu_localizer.clone(),
3858 ctx: OptionRefAny::None,
3859 };
3860
3861 let callback_info = CallbackInfo::new(
3862 &ref_data,
3863 &callback_changes,
3864 hit_dom_node,
3865 cursor_relative_to_item,
3866 cursor_in_viewport,
3867 );
3868
3869 ret.callbacks_update_screen = (callback.cb)(data.clone(), callback_info);
3870
3871 let collected_changes = callback_changes
3873 .lock()
3874 .map(|mut guard| core::mem::take(&mut *guard))
3875 .unwrap_or_default();
3876
3877 let change_result = self.apply_callback_changes(
3879 collected_changes,
3880 current_window_state,
3881 image_cache,
3882 system_fonts,
3883 );
3884
3885 self.queue_iframe_updates(change_result.iframes_to_update);
3887
3888 ret.stop_propagation = change_result.stop_propagation;
3889 ret.prevent_default = change_result.prevent_default;
3890 ret.tooltips_to_show = change_result.tooltips_to_show;
3891 ret.hide_tooltip = change_result.hide_tooltip;
3892
3893 if change_result.hit_test_update_requested.is_some() {
3895 ret.hit_test_update_requested = change_result.hit_test_update_requested;
3896 }
3897
3898 ret_timers.extend(change_result.timers);
3899 ret_threads.extend(change_result.threads);
3900 ret_timers_removed.extend(change_result.timers_removed);
3901 ret_threads_removed.extend(change_result.threads_removed);
3902 ret_words_changed.extend(change_result.words_changed);
3903 ret_images_changed.extend(change_result.images_changed);
3904 ret_image_masks_changed.extend(change_result.image_masks_changed);
3905 ret_css_properties_changed.extend(change_result.css_properties_changed);
3906 ret_nodes_scrolled_in_callbacks.append(&mut change_result.nodes_scrolled.clone());
3907
3908 if change_result.modified_window_state != *current_window_state {
3909 ret_modified_window_state = change_result.modified_window_state;
3910 }
3911
3912 new_focus_target = change_result.focus_target.or(new_focus_target);
3913
3914 if !ret_timers.is_empty() {
3915 ret.timers = Some(ret_timers);
3916 }
3917 if !ret_threads.is_empty() {
3918 ret.threads = Some(ret_threads);
3919 }
3920 if ret_modified_window_state != ret_window_state {
3921 ret.modified_window_state = Some(ret_modified_window_state);
3922 }
3923 if !ret_threads_removed.is_empty() {
3924 ret.threads_removed = Some(ret_threads_removed);
3925 }
3926 if !ret_timers_removed.is_empty() {
3927 ret.timers_removed = Some(ret_timers_removed);
3928 }
3929 if !ret_words_changed.is_empty() {
3930 ret.words_changed = Some(ret_words_changed);
3931 }
3932 if !ret_images_changed.is_empty() {
3933 ret.images_changed = Some(ret_images_changed);
3934 }
3935 if !ret_image_masks_changed.is_empty() {
3936 ret.image_masks_changed = Some(ret_image_masks_changed);
3937 }
3938 if !ret_css_properties_changed.is_empty() {
3939 ret.css_properties_changed = Some(ret_css_properties_changed);
3940 }
3941 if !ret_nodes_scrolled_in_callbacks.is_empty() {
3942 ret.nodes_scrolled_in_callbacks = Some(ret_nodes_scrolled_in_callbacks);
3943 }
3944
3945 if let Some(ft) = new_focus_target {
3946 if let Ok(new_focus_node) = crate::managers::focus_cursor::resolve_focus_target(
3947 &ft,
3948 &self.layout_results,
3949 self.focus_manager.get_focused_node().copied(),
3950 ) {
3951 ret.update_focused_node = match new_focus_node {
3952 Some(node) => FocusUpdateRequest::FocusNode(node),
3953 None => FocusUpdateRequest::ClearFocus,
3954 };
3955 }
3956 }
3957
3958 return ret;
3959 }
3960
3961 pub fn invoke_menu_callback(
3963 &mut self,
3964 menu_callback: &mut MenuCallback,
3965 hit_dom_node: DomNodeId,
3966 current_window_handle: &RawWindowHandle,
3967 gl_context: &OptionGlContextPtr,
3968 image_cache: &mut ImageCache,
3969 system_fonts: &mut FcFontCache,
3970 system_style: std::sync::Arc<azul_css::system::SystemStyle>,
3971 system_callbacks: &ExternalSystemCallbacks,
3972 previous_window_state: &Option<FullWindowState>,
3973 current_window_state: &FullWindowState,
3974 renderer_resources: &RendererResources,
3975 ) -> CallCallbacksResult {
3976 use crate::callbacks::{CallCallbacksResult, CallbackInfo, MenuCallback};
3977
3978 let mut ret = CallCallbacksResult {
3979 should_scroll_render: false,
3980 callbacks_update_screen: Update::DoNothing,
3981 modified_window_state: None,
3982 css_properties_changed: None,
3983 words_changed: None,
3984 images_changed: None,
3985 image_masks_changed: None,
3986 image_callbacks_changed: None,
3987 nodes_scrolled_in_callbacks: None,
3988 update_focused_node: FocusUpdateRequest::NoChange,
3989 timers: None,
3990 threads: None,
3991 timers_removed: None,
3992 threads_removed: None,
3993 windows_created: Vec::new(),
3994 menus_to_open: Vec::new(),
3995 tooltips_to_show: Vec::new(),
3996 hide_tooltip: false,
3997 cursor_changed: false,
3998 stop_propagation: false,
3999 prevent_default: false,
4000 hit_test_update_requested: None,
4001 queued_window_states: Vec::new(),
4002 text_input_triggered: Vec::new(),
4003 };
4004
4005 let mut ret_modified_window_state = current_window_state.clone();
4006 let ret_window_state = ret_modified_window_state.clone();
4007 let mut ret_timers = FastHashMap::new();
4008 let mut ret_timers_removed = FastBTreeSet::new();
4009 let mut ret_threads = FastHashMap::new();
4010 let mut ret_threads_removed = FastBTreeSet::new();
4011 let mut ret_words_changed = BTreeMap::new();
4012 let mut ret_images_changed = BTreeMap::new();
4013 let mut ret_image_masks_changed = BTreeMap::new();
4014 let mut ret_css_properties_changed = BTreeMap::new();
4015 let mut ret_nodes_scrolled_in_callbacks = BTreeMap::new();
4016 let mut new_focus_target = None;
4017 let mut stop_propagation = false;
4018 let current_scroll_states = self.get_nested_scroll_states(DomId::ROOT_ID);
4019
4020 let cursor_relative_to_item = OptionLogicalPosition::None;
4021 let cursor_in_viewport = OptionLogicalPosition::None;
4022
4023 let callback_changes = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
4025
4026 let ref_data = crate::callbacks::CallbackInfoRefData {
4028 layout_window: self,
4029 renderer_resources,
4030 previous_window_state,
4031 current_window_state,
4032 gl_context,
4033 current_scroll_manager: ¤t_scroll_states,
4034 current_window_handle,
4035 system_callbacks,
4036 system_style,
4037 #[cfg(feature = "icu")]
4038 icu_localizer: self.icu_localizer.clone(),
4039 ctx: OptionRefAny::None,
4040 };
4041
4042 let callback_info = CallbackInfo::new(
4043 &ref_data,
4044 &callback_changes,
4045 hit_dom_node,
4046 cursor_relative_to_item,
4047 cursor_in_viewport,
4048 );
4049
4050 ret.callbacks_update_screen =
4051 (menu_callback.callback.cb)(menu_callback.refany.clone(), callback_info);
4052
4053 let collected_changes = callback_changes
4055 .lock()
4056 .map(|mut guard| core::mem::take(&mut *guard))
4057 .unwrap_or_default();
4058
4059 let change_result = self.apply_callback_changes(
4061 collected_changes,
4062 current_window_state,
4063 image_cache,
4064 system_fonts,
4065 );
4066
4067 self.queue_iframe_updates(change_result.iframes_to_update);
4069
4070 ret.stop_propagation = change_result.stop_propagation;
4071 ret.prevent_default = change_result.prevent_default;
4072 ret.tooltips_to_show = change_result.tooltips_to_show;
4073 ret.hide_tooltip = change_result.hide_tooltip;
4074
4075 if change_result.hit_test_update_requested.is_some() {
4077 ret.hit_test_update_requested = change_result.hit_test_update_requested;
4078 }
4079
4080 ret_timers.extend(change_result.timers);
4081 ret_threads.extend(change_result.threads);
4082 ret_timers_removed.extend(change_result.timers_removed);
4083 ret_threads_removed.extend(change_result.threads_removed);
4084 ret_words_changed.extend(change_result.words_changed);
4085 ret_images_changed.extend(change_result.images_changed);
4086 ret_image_masks_changed.extend(change_result.image_masks_changed);
4087 ret_css_properties_changed.extend(change_result.css_properties_changed);
4088 ret_nodes_scrolled_in_callbacks.append(&mut change_result.nodes_scrolled.clone());
4089
4090 if change_result.modified_window_state != *current_window_state {
4091 ret_modified_window_state = change_result.modified_window_state;
4092 }
4093
4094 new_focus_target = change_result.focus_target.or(new_focus_target);
4095
4096 if !ret_timers.is_empty() {
4097 ret.timers = Some(ret_timers);
4098 }
4099 if !ret_threads.is_empty() {
4100 ret.threads = Some(ret_threads);
4101 }
4102 if ret_modified_window_state != ret_window_state {
4103 ret.modified_window_state = Some(ret_modified_window_state);
4104 }
4105 if !ret_threads_removed.is_empty() {
4106 ret.threads_removed = Some(ret_threads_removed);
4107 }
4108 if !ret_timers_removed.is_empty() {
4109 ret.timers_removed = Some(ret_timers_removed);
4110 }
4111 if !ret_words_changed.is_empty() {
4112 ret.words_changed = Some(ret_words_changed);
4113 }
4114 if !ret_images_changed.is_empty() {
4115 ret.images_changed = Some(ret_images_changed);
4116 }
4117 if !ret_image_masks_changed.is_empty() {
4118 ret.image_masks_changed = Some(ret_image_masks_changed);
4119 }
4120 if !ret_css_properties_changed.is_empty() {
4121 ret.css_properties_changed = Some(ret_css_properties_changed);
4122 }
4123 if !ret_nodes_scrolled_in_callbacks.is_empty() {
4124 ret.nodes_scrolled_in_callbacks = Some(ret_nodes_scrolled_in_callbacks);
4125 }
4126
4127 if let Some(ft) = new_focus_target {
4128 if let Ok(new_focus_node) = crate::managers::focus_cursor::resolve_focus_target(
4129 &ft,
4130 &self.layout_results,
4131 self.focus_manager.get_focused_node().copied(),
4132 ) {
4133 ret.update_focused_node = match new_focus_node {
4134 Some(node) => FocusUpdateRequest::FocusNode(node),
4135 None => FocusUpdateRequest::ClearFocus,
4136 };
4137 }
4138 }
4139
4140 return ret;
4141 }
4142}
4143
4144#[cfg(feature = "icu")]
4147impl LayoutWindow {
4148 pub fn set_icu_locale(&mut self, locale: &str) {
4156 self.icu_localizer.set_locale(locale);
4157 }
4158
4159 pub fn init_icu_from_system_style(&mut self, system_style: &azul_css::system::SystemStyle) {
4163 self.icu_localizer = IcuLocalizerHandle::from_system_language(&system_style.language);
4164 }
4165
4166 pub fn get_icu_localizer(&self) -> IcuLocalizerHandle {
4170 self.icu_localizer.clone()
4171 }
4172
4173 pub fn load_icu_data_blob(&mut self, data: Vec<u8>) -> bool {
4178 self.icu_localizer.load_data_blob(&data)
4179 }
4180}
4181
4182#[cfg(test)]
4183mod tests {
4184 use super::*;
4185 use crate::{thread::Thread, timer::Timer};
4186
4187 #[test]
4188 fn test_timer_add_remove() {
4189 let fc_cache = FcFontCache::default();
4190 let mut window = LayoutWindow::new(fc_cache).unwrap();
4191
4192 let timer_id = TimerId { id: 1 };
4193 let timer = Timer::default();
4194
4195 window.add_timer(timer_id, timer);
4197 assert!(window.get_timer(&timer_id).is_some());
4198 assert_eq!(window.get_timer_ids().len(), 1);
4199
4200 let removed = window.remove_timer(&timer_id);
4202 assert!(removed.is_some());
4203 assert!(window.get_timer(&timer_id).is_none());
4204 assert_eq!(window.get_timer_ids().len(), 0);
4205 }
4206
4207 #[test]
4208 fn test_timer_get_mut() {
4209 let fc_cache = FcFontCache::default();
4210 let mut window = LayoutWindow::new(fc_cache).unwrap();
4211
4212 let timer_id = TimerId { id: 1 };
4213 let timer = Timer::default();
4214
4215 window.add_timer(timer_id, timer);
4216
4217 let timer_mut = window.get_timer_mut(&timer_id);
4219 assert!(timer_mut.is_some());
4220 }
4221
4222 #[test]
4223 fn test_multiple_timers() {
4224 let fc_cache = FcFontCache::default();
4225 let mut window = LayoutWindow::new(fc_cache).unwrap();
4226
4227 let timer1 = TimerId { id: 1 };
4228 let timer2 = TimerId { id: 2 };
4229 let timer3 = TimerId { id: 3 };
4230
4231 window.add_timer(timer1, Timer::default());
4232 window.add_timer(timer2, Timer::default());
4233 window.add_timer(timer3, Timer::default());
4234
4235 assert_eq!(window.get_timer_ids().len(), 3);
4236
4237 window.remove_timer(&timer2);
4238 assert_eq!(window.get_timer_ids().len(), 2);
4239 assert!(window.get_timer(&timer1).is_some());
4240 assert!(window.get_timer(&timer2).is_none());
4241 assert!(window.get_timer(&timer3).is_some());
4242 }
4243
4244 #[test]
4249 fn test_gpu_cache_management() {
4250 let fc_cache = FcFontCache::default();
4251 let mut window = LayoutWindow::new(fc_cache).unwrap();
4252
4253 let dom_id = DomId { inner: 0 };
4254
4255 assert!(window.get_gpu_cache(&dom_id).is_none());
4257
4258 let cache = window.get_or_create_gpu_cache(dom_id);
4260 assert!(cache.transform_keys.is_empty());
4261
4262 assert!(window.get_gpu_cache(&dom_id).is_some());
4264
4265 let cache_mut = window.get_gpu_cache_mut(&dom_id);
4267 assert!(cache_mut.is_some());
4268 }
4269
4270 #[test]
4271 fn test_gpu_cache_multiple_doms() {
4272 let fc_cache = FcFontCache::default();
4273 let mut window = LayoutWindow::new(fc_cache).unwrap();
4274
4275 let dom1 = DomId { inner: 0 };
4276 let dom2 = DomId { inner: 1 };
4277
4278 window.get_or_create_gpu_cache(dom1);
4279 window.get_or_create_gpu_cache(dom2);
4280
4281 assert!(window.get_gpu_cache(&dom1).is_some());
4282 assert!(window.get_gpu_cache(&dom2).is_some());
4283 }
4284
4285 #[test]
4286 fn test_compute_cursor_type_empty_hit_test() {
4287 use crate::hit_test::FullHitTest;
4288
4289 let fc_cache = FcFontCache::default();
4290 let window = LayoutWindow::new(fc_cache).unwrap();
4291
4292 let empty_hit = FullHitTest::empty(None);
4293 let cursor_test = window.compute_cursor_type_hit_test(&empty_hit);
4294
4295 assert_eq!(
4297 cursor_test.cursor_icon,
4298 azul_core::window::MouseCursorType::Default
4299 );
4300 assert!(cursor_test.cursor_node.is_none());
4301 }
4302
4303 #[test]
4304 fn test_layout_result_access() {
4305 let fc_cache = FcFontCache::default();
4306 let window = LayoutWindow::new(fc_cache).unwrap();
4307
4308 let dom_id = DomId { inner: 0 };
4309
4310 assert!(window.get_layout_result(&dom_id).is_none());
4312 assert_eq!(window.get_dom_ids().len(), 0);
4313 }
4314
4315 #[test]
4318 fn test_scroll_manager_initialization() {
4319 let fc_cache = FcFontCache::default();
4320 let window = LayoutWindow::new(fc_cache).unwrap();
4321
4322 let dom_id = DomId::ROOT_ID;
4323 let node_id = NodeId::new(0);
4324
4325 let scroll_offsets = window.scroll_manager.get_scroll_states_for_dom(dom_id);
4327 assert!(scroll_offsets.is_empty());
4328
4329 let offset = window.scroll_manager.get_current_offset(dom_id, node_id);
4331 assert_eq!(offset, None);
4332 }
4333
4334 #[test]
4335 fn test_scroll_manager_tick_updates_activity() {
4336 let fc_cache = FcFontCache::default();
4337 let mut window = LayoutWindow::new(fc_cache).unwrap();
4338
4339 let dom_id = DomId::ROOT_ID;
4340 let node_id = NodeId::new(0);
4341
4342 let scroll_event = crate::managers::scroll_state::ScrollEvent {
4344 dom_id,
4345 node_id,
4346 delta: LogicalPosition::new(10.0, 20.0),
4347 source: azul_core::events::EventSource::User,
4348 duration: None,
4349 easing: EasingFunction::Linear,
4350 };
4351
4352 #[cfg(feature = "std")]
4353 let now = Instant::System(std::time::Instant::now().into());
4354 #[cfg(not(feature = "std"))]
4355 let now = Instant::Tick(azul_core::task::SystemTick { tick_counter: 0 });
4356
4357 let did_scroll = window
4358 .scroll_manager
4359 .process_scroll_event(scroll_event, now.clone());
4360
4361 assert!(did_scroll);
4363 }
4364
4365 #[test]
4366 fn test_scroll_manager_programmatic_scroll() {
4367 let fc_cache = FcFontCache::default();
4368 let mut window = LayoutWindow::new(fc_cache).unwrap();
4369
4370 let dom_id = DomId::ROOT_ID;
4371 let node_id = NodeId::new(0);
4372
4373 #[cfg(feature = "std")]
4374 let now = Instant::System(std::time::Instant::now().into());
4375 #[cfg(not(feature = "std"))]
4376 let now = Instant::Tick(azul_core::task::SystemTick { tick_counter: 0 });
4377
4378 window.scroll_manager.scroll_to(
4380 dom_id,
4381 node_id,
4382 LogicalPosition::new(100.0, 200.0),
4383 Duration::System(SystemTimeDiff::from_millis(300)),
4384 EasingFunction::EaseOut,
4385 now.clone(),
4386 );
4387
4388 let tick_result = window.scroll_manager.tick(now);
4389
4390 assert!(tick_result.needs_repaint);
4392 }
4393
4394 #[test]
4395 fn test_scroll_manager_iframe_edge_detection() {
4396 }
4404
4405 #[test]
4406 fn test_scroll_manager_iframe_invocation_tracking() {
4407 }
4414
4415 #[test]
4416 fn test_scrollbar_opacity_fading() {
4417 }
4428
4429 #[test]
4430 fn test_iframe_callback_reason_initial_render() {
4431 }
4439
4440 #[test]
4441 fn test_gpu_cache_scrollbar_opacity_keys() {
4442 let fc_cache = FcFontCache::default();
4443 let mut window = LayoutWindow::new(fc_cache).unwrap();
4444
4445 let dom_id = DomId::ROOT_ID;
4446 let node_id = NodeId::new(0);
4447
4448 let gpu_cache = window.get_or_create_gpu_cache(dom_id);
4450
4451 assert!(gpu_cache.scrollbar_v_opacity_keys.is_empty());
4453 assert!(gpu_cache.scrollbar_h_opacity_keys.is_empty());
4454
4455 let opacity_key = azul_core::resources::OpacityKey::unique();
4457 gpu_cache
4458 .scrollbar_v_opacity_keys
4459 .insert((dom_id, node_id), opacity_key);
4460 gpu_cache
4461 .scrollbar_v_opacity_values
4462 .insert((dom_id, node_id), 1.0);
4463
4464 assert_eq!(gpu_cache.scrollbar_v_opacity_keys.len(), 1);
4466 assert_eq!(
4467 gpu_cache.scrollbar_v_opacity_values.get(&(dom_id, node_id)),
4468 Some(&1.0)
4469 );
4470 }
4471
4472 #[test]
4473 fn test_frame_lifecycle_begin_end() {
4474 }
4485}
4486
4487impl LayoutWindow {
4489 pub fn find_next_text_node(
4502 &self,
4503 dom_id: &DomId,
4504 current_node: NodeId,
4505 ) -> Option<(DomId, NodeId)> {
4506 let layout_result = self.get_layout_result(dom_id)?;
4507 let styled_dom = &layout_result.styled_dom;
4508
4509 let start_idx = current_node.index() + 1;
4511 let node_hierarchy = &styled_dom.node_hierarchy;
4512
4513 for i in start_idx..node_hierarchy.len() {
4514 let node_id = NodeId::new(i);
4515
4516 if self.node_has_text_content(styled_dom, node_id) {
4518 if self.is_text_selectable(styled_dom, node_id) {
4520 return Some((*dom_id, node_id));
4521 }
4522 }
4523 }
4524
4525 None
4526 }
4527
4528 pub fn find_prev_text_node(
4541 &self,
4542 dom_id: &DomId,
4543 current_node: NodeId,
4544 ) -> Option<(DomId, NodeId)> {
4545 let layout_result = self.get_layout_result(dom_id)?;
4546 let styled_dom = &layout_result.styled_dom;
4547
4548 let current_idx = current_node.index();
4550
4551 for i in (0..current_idx).rev() {
4552 let node_id = NodeId::new(i);
4553
4554 if self.node_has_text_content(styled_dom, node_id) {
4556 if self.is_text_selectable(styled_dom, node_id) {
4558 return Some((*dom_id, node_id));
4559 }
4560 }
4561 }
4562
4563 None
4564 }
4565
4566 fn find_last_text_child(&self, dom_id: DomId, parent_node_id: NodeId) -> Option<NodeId> {
4572 let layout_result = self.layout_results.get(&dom_id)?;
4573 let styled_dom = &layout_result.styled_dom;
4574 let node_data_container = styled_dom.node_data.as_container();
4575 let hierarchy_container = styled_dom.node_hierarchy.as_container();
4576
4577 let parent_type = node_data_container[parent_node_id].get_node_type();
4579 if matches!(parent_type, NodeType::Text(_)) {
4580 return Some(parent_node_id);
4581 }
4582
4583 let parent_item = &hierarchy_container[parent_node_id];
4585 let mut last_text_child: Option<NodeId> = None;
4586 let mut current_child = parent_item.first_child_id(parent_node_id);
4587 while let Some(child_id) = current_child {
4588 let child_type = node_data_container[child_id].get_node_type();
4589 if matches!(child_type, NodeType::Text(_)) {
4590 last_text_child = Some(child_id);
4591 }
4592 current_child = hierarchy_container[child_id].next_sibling_id();
4593 }
4594
4595 last_text_child
4596 }
4597
4598 fn node_has_text_content(&self, styled_dom: &StyledDom, node_id: NodeId) -> bool {
4600 let node_data_container = styled_dom.node_data.as_container();
4602 let node_type = node_data_container[node_id].get_node_type();
4603 if matches!(node_type, NodeType::Text(_)) {
4604 return true;
4605 }
4606
4607 let hierarchy_container = styled_dom.node_hierarchy.as_container();
4609 let node_item = &hierarchy_container[node_id];
4610
4611 let mut current_child = node_item.first_child_id(node_id);
4613 while let Some(child_id) = current_child {
4614 let child_type = node_data_container[child_id].get_node_type();
4615 if matches!(child_type, NodeType::Text(_)) {
4616 return true;
4617 }
4618
4619 current_child = hierarchy_container[child_id].next_sibling_id();
4621 }
4622
4623 false
4624 }
4625
4626 fn is_text_selectable(&self, styled_dom: &StyledDom, node_id: NodeId) -> bool {
4628 let node_state = &styled_dom.styled_nodes.as_container()[node_id].styled_node_state;
4629 crate::solver3::getters::is_text_selectable(styled_dom, node_id, node_state)
4630 }
4631
4632 #[cfg(feature = "a11y")]
4651 pub fn process_accessibility_action(
4652 &mut self,
4653 dom_id: DomId,
4654 node_id: NodeId,
4655 action: azul_core::dom::AccessibilityAction,
4656 now: std::time::Instant,
4657 ) -> BTreeMap<DomNodeId, (Vec<azul_core::events::EventFilter>, bool)> {
4658 use crate::managers::text_input::TextInputSource;
4659
4660 let mut affected_nodes = BTreeMap::new();
4661
4662 match action {
4663 AccessibilityAction::Focus => {
4665 let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(node_id));
4666 let dom_node_id = DomNodeId {
4667 dom: dom_id,
4668 node: hierarchy_id,
4669 };
4670 self.focus_manager.set_focused_node(Some(dom_node_id));
4671
4672 if let Some(layout_result) = self.layout_results.get(&dom_id) {
4674 if let Some(styled_node) = layout_result
4675 .styled_dom
4676 .node_data
4677 .as_ref()
4678 .get(node_id.index())
4679 {
4680 let is_contenteditable = styled_node.contenteditable
4684 || styled_node.attributes.as_ref().iter().any(|attr| {
4685 matches!(attr, azul_core::dom::AttributeType::ContentEditable(_))
4686 });
4687
4688 if is_contenteditable {
4689 let inline_layout = self.get_inline_layout_for_node(dom_id, node_id).cloned();
4692 if inline_layout.is_some() {
4693 self.cursor_manager.initialize_cursor_at_end(
4694 dom_id,
4695 node_id,
4696 inline_layout.as_ref(),
4697 );
4698
4699 self.scroll_cursor_into_view_if_needed(dom_id, node_id, now);
4701 }
4702 } else {
4703 self.cursor_manager.clear();
4705 }
4706 }
4707 }
4708
4709 self.scroll_to_node_if_needed(dom_id, node_id, now);
4711 }
4712 AccessibilityAction::Blur => {
4713 self.focus_manager.clear_focus();
4714 self.cursor_manager.clear();
4715 }
4716 AccessibilityAction::SetSequentialFocusNavigationStartingPoint => {
4717 let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(node_id));
4718 let dom_node_id = DomNodeId {
4719 dom: dom_id,
4720 node: hierarchy_id,
4721 };
4722 self.focus_manager.set_focused_node(Some(dom_node_id));
4723 self.cursor_manager.clear();
4725 }
4726
4727 AccessibilityAction::ScrollIntoView => {
4729 self.scroll_to_node_if_needed(dom_id, node_id, now);
4730 }
4731 AccessibilityAction::ScrollUp => {
4732 self.scroll_manager.scroll_by(
4733 dom_id,
4734 node_id,
4735 LogicalPosition { x: 0.0, y: -100.0 },
4736 std::time::Duration::from_millis(200).into(),
4737 azul_core::events::EasingFunction::EaseOut,
4738 now.into(),
4739 );
4740 }
4741 AccessibilityAction::ScrollDown => {
4742 self.scroll_manager.scroll_by(
4743 dom_id,
4744 node_id,
4745 LogicalPosition { x: 0.0, y: 100.0 },
4746 std::time::Duration::from_millis(200).into(),
4747 azul_core::events::EasingFunction::EaseOut,
4748 now.into(),
4749 );
4750 }
4751 AccessibilityAction::ScrollLeft => {
4752 self.scroll_manager.scroll_by(
4753 dom_id,
4754 node_id,
4755 LogicalPosition { x: -100.0, y: 0.0 },
4756 std::time::Duration::from_millis(200).into(),
4757 azul_core::events::EasingFunction::EaseOut,
4758 now.into(),
4759 );
4760 }
4761 AccessibilityAction::ScrollRight => {
4762 self.scroll_manager.scroll_by(
4763 dom_id,
4764 node_id,
4765 LogicalPosition { x: 100.0, y: 0.0 },
4766 std::time::Duration::from_millis(200).into(),
4767 azul_core::events::EasingFunction::EaseOut,
4768 now.into(),
4769 );
4770 }
4771 AccessibilityAction::ScrollUp => {
4772 if let Some(size) = self.get_node_used_size_a11y(dom_id, node_id) {
4774 let scroll_amount = size.height.min(100.0); self.scroll_manager.scroll_by(
4776 dom_id,
4777 node_id,
4778 LogicalPosition {
4779 x: 0.0,
4780 y: -scroll_amount,
4781 },
4782 std::time::Duration::from_millis(300).into(),
4783 azul_core::events::EasingFunction::EaseInOut,
4784 now.into(),
4785 );
4786 }
4787 }
4788 AccessibilityAction::ScrollDown => {
4789 if let Some(size) = self.get_node_used_size_a11y(dom_id, node_id) {
4791 let scroll_amount = size.height.min(100.0); self.scroll_manager.scroll_by(
4793 dom_id,
4794 node_id,
4795 LogicalPosition {
4796 x: 0.0,
4797 y: scroll_amount,
4798 },
4799 std::time::Duration::from_millis(300).into(),
4800 azul_core::events::EasingFunction::EaseInOut,
4801 now.into(),
4802 );
4803 }
4804 }
4805 AccessibilityAction::SetScrollOffset(pos) => {
4806 self.scroll_manager.scroll_to(
4807 dom_id,
4808 node_id,
4809 pos,
4810 std::time::Duration::from_millis(0).into(),
4811 azul_core::events::EasingFunction::Linear,
4812 now.into(),
4813 );
4814 }
4815 AccessibilityAction::ScrollToPoint(pos) => {
4816 self.scroll_manager.scroll_to(
4817 dom_id,
4818 node_id,
4819 pos,
4820 std::time::Duration::from_millis(300).into(),
4821 azul_core::events::EasingFunction::EaseInOut,
4822 now.into(),
4823 );
4824 }
4825
4826 AccessibilityAction::Default => {
4830 let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(node_id));
4832 let dom_node_id = DomNodeId {
4833 dom: dom_id,
4834 node: hierarchy_id,
4835 };
4836
4837 let event_filter = if let Some(layout_result) = self.layout_results.get(&dom_id) {
4839 if let Some(styled_node) = layout_result
4840 .styled_dom
4841 .node_data
4842 .as_ref()
4843 .get(node_id.index())
4844 {
4845 let has_default_callback =
4846 styled_node.callbacks.as_ref().iter().any(|cb| {
4847 matches!(cb.event, EventFilter::Hover(HoverEventFilter::MouseUp))
4849 });
4850
4851 if has_default_callback {
4852 EventFilter::Hover(HoverEventFilter::MouseUp)
4853 } else {
4854 EventFilter::Hover(HoverEventFilter::MouseUp)
4855 }
4856 } else {
4857 EventFilter::Hover(HoverEventFilter::MouseUp)
4858 }
4859 } else {
4860 EventFilter::Hover(HoverEventFilter::MouseUp)
4861 };
4862
4863 affected_nodes.insert(dom_node_id, (vec![event_filter], false));
4864 }
4865
4866 AccessibilityAction::Increment | AccessibilityAction::Decrement => {
4867 let is_increment = matches!(action, AccessibilityAction::Increment);
4877
4878 let current_value = if let Some(layout_result) = self.layout_results.get(&dom_id) {
4880 if let Some(styled_node) = layout_result
4881 .styled_dom
4882 .node_data
4883 .as_ref()
4884 .get(node_id.index())
4885 {
4886 styled_node
4888 .attributes
4889 .as_ref()
4890 .iter()
4891 .find_map(|attr| {
4892 if let AttributeType::Value(v) = attr {
4893 Some(v.as_str().to_string())
4894 } else {
4895 None
4896 }
4897 })
4898 .or_else(|| {
4899 if let NodeType::Text(text) = styled_node.get_node_type() {
4901 Some(text.as_str().to_string())
4902 } else {
4903 None
4904 }
4905 })
4906 } else {
4907 None
4908 }
4909 } else {
4910 None
4911 };
4912
4913 if let Some(value_str) = current_value {
4915 let parsed: Result<f64, _> = value_str.trim().parse();
4916
4917 let new_value_str = if let Ok(num) = parsed {
4918 let new_num = if is_increment { num + 1.0 } else { num - 1.0 };
4920 if num.fract() == 0.0 {
4922 format!("{}", new_num as i64)
4923 } else {
4924 format!("{}", new_num)
4925 }
4926 } else {
4927 if is_increment {
4929 "1".to_string()
4930 } else {
4931 "-1".to_string()
4932 }
4933 };
4934
4935 let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(node_id));
4937 let dom_node_id = DomNodeId {
4938 dom: dom_id,
4939 node: hierarchy_id,
4940 };
4941
4942 let old_inline_content = self.get_text_before_textinput(dom_id, node_id);
4944 let old_text = self.extract_text_from_inline_content(&old_inline_content);
4945
4946 self.text_input_manager.record_input(
4948 dom_node_id,
4949 new_value_str,
4950 old_text,
4951 TextInputSource::Accessibility,
4952 );
4953
4954 affected_nodes.insert(
4956 dom_node_id,
4957 (vec![EventFilter::Focus(FocusEventFilter::TextInput)], false),
4958 );
4959 }
4960 }
4961
4962 AccessibilityAction::Collapse | AccessibilityAction::Expand => {
4963 let event_type = match action {
4965 AccessibilityAction::Collapse => On::Collapse,
4966 AccessibilityAction::Expand => On::Expand,
4967 _ => unreachable!(),
4968 };
4969
4970 if let Some(layout_result) = self.layout_results.get(&dom_id) {
4972 if let Some(styled_node) = layout_result
4973 .styled_dom
4974 .node_data
4975 .as_ref()
4976 .get(node_id.index())
4977 {
4978 let has_callback = styled_node
4980 .callbacks
4981 .as_ref()
4982 .iter()
4983 .any(|cb| cb.event == event_type.into());
4984
4985 let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(node_id));
4986 let dom_node_id = DomNodeId {
4987 dom: dom_id,
4988 node: hierarchy_id,
4989 };
4990
4991 if has_callback {
4992 affected_nodes.insert(dom_node_id, (vec![event_type.into()], false));
4994 } else {
4995 affected_nodes.insert(
4997 dom_node_id,
4998 (vec![EventFilter::Hover(HoverEventFilter::MouseUp)], false),
4999 );
5000 }
5001 }
5002 }
5003 }
5004
5005 AccessibilityAction::ShowContextMenu => {
5007 let layout_result = match self.layout_results.get(&dom_id) {
5009 Some(lr) => lr,
5010 None => {
5011 return affected_nodes;
5012 }
5013 };
5014
5015 let styled_node = match layout_result
5017 .styled_dom
5018 .node_data
5019 .as_ref()
5020 .get(node_id.index())
5021 {
5022 Some(node) => node,
5023 None => {
5024 return affected_nodes;
5025 }
5026 };
5027
5028 let has_context_menu = styled_node.get_context_menu().is_some();
5030
5031 if has_context_menu {
5032 } else {
5035 }
5037 }
5038
5039 AccessibilityAction::ReplaceSelectedText(ref text) => {
5041 let nodes = self.edit_text_node(
5042 dom_id,
5043 node_id,
5044 TextEditType::ReplaceSelection(text.as_str().to_string()),
5045 );
5046 for node in nodes {
5047 affected_nodes.insert(node, (Vec::new(), true)); }
5049 }
5050 AccessibilityAction::SetValue(ref text) => {
5051 let nodes = self.edit_text_node(
5052 dom_id,
5053 node_id,
5054 TextEditType::SetValue(text.as_str().to_string()),
5055 );
5056 for node in nodes {
5057 affected_nodes.insert(node, (Vec::new(), true));
5058 }
5059 }
5060 AccessibilityAction::SetNumericValue(value) => {
5061 let nodes = self.edit_text_node(
5062 dom_id,
5063 node_id,
5064 TextEditType::SetNumericValue(value.get() as f64),
5065 );
5066 for node in nodes {
5067 affected_nodes.insert(node, (Vec::new(), true));
5068 }
5069 }
5070 AccessibilityAction::SetTextSelection(selection) => {
5071 let text_layout = self.get_node_inline_layout(dom_id, node_id);
5073
5074 if let Some(inline_layout) = text_layout {
5075 let start_cursor = self.byte_offset_to_cursor(
5077 inline_layout.as_ref(),
5078 selection.selection_start as u32,
5079 );
5080 let end_cursor = self.byte_offset_to_cursor(
5081 inline_layout.as_ref(),
5082 selection.selection_end as u32,
5083 );
5084
5085 if let (Some(start), Some(end)) = (start_cursor, end_cursor) {
5086 let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(node_id));
5087 let dom_node_id = DomNodeId {
5088 dom: dom_id,
5089 node: hierarchy_id,
5090 };
5091
5092 if start == end {
5093 self.cursor_manager.move_cursor_to(start, dom_id, node_id);
5095
5096 self.selection_manager.clear_selection(&dom_id);
5098 } else {
5099 let selection = Selection::Range(SelectionRange { start, end });
5101
5102 let selection_state = SelectionState {
5103 selections: vec![selection].into(),
5104 node_id: dom_node_id,
5105 };
5106
5107 self.selection_manager
5109 .set_selection(dom_id, selection_state);
5110
5111 self.cursor_manager.move_cursor_to(start, dom_id, node_id);
5113 }
5114 } else {
5115 }
5117 } else {
5118 }
5120 }
5121
5122 AccessibilityAction::ShowTooltip | AccessibilityAction::HideTooltip => {
5124 }
5126
5127 AccessibilityAction::CustomAction(_id) => {
5128 }
5130 }
5131
5132 self.sync_cursor_to_selection_manager();
5134
5135 affected_nodes
5136 }
5137
5138 pub fn record_text_input(
5160 &mut self,
5161 text_input: &str,
5162 ) -> BTreeMap<azul_core::dom::DomNodeId, (Vec<azul_core::events::EventFilter>, bool)> {
5163 use std::collections::BTreeMap;
5164
5165 use crate::managers::text_input::TextInputSource;
5166
5167 println!("[record_text_input] Called with text: '{}'", text_input);
5168
5169 let mut affected_nodes = BTreeMap::new();
5170
5171 if text_input.is_empty() {
5172 println!("[record_text_input] Empty text, returning empty");
5173 return affected_nodes;
5174 }
5175
5176 let focused_node = match self.focus_manager.get_focused_node().copied() {
5178 Some(node) => {
5179 println!("[record_text_input] Focused node: {:?}", node);
5180 node
5181 },
5182 None => {
5183 println!("[record_text_input] ERROR: No focused node!");
5184 return affected_nodes;
5185 }
5186 };
5187
5188 let node_id = match focused_node.node.into_crate_internal() {
5189 Some(id) => {
5190 println!("[record_text_input] Node ID: {:?}", id);
5191 id
5192 },
5193 None => {
5194 println!("[record_text_input] ERROR: Invalid node ID");
5195 return affected_nodes;
5196 }
5197 };
5198
5199 let old_inline_content = self.get_text_before_textinput(focused_node.dom, node_id);
5201 let old_text = self.extract_text_from_inline_content(&old_inline_content);
5202 println!("[record_text_input] Old text: '{}' ({} bytes)", old_text, old_text.len());
5203
5204 println!("[record_text_input] Recording input in TextInputManager...");
5206 self.text_input_manager.record_input(
5207 focused_node,
5208 text_input.to_string(),
5209 old_text,
5210 TextInputSource::Keyboard, );
5212 println!("[record_text_input] Input recorded successfully");
5213
5214 let text_input_event = vec![EventFilter::Focus(FocusEventFilter::TextInput)];
5216
5217 affected_nodes.insert(focused_node, (text_input_event, false)); println!("[record_text_input] Returning {} affected nodes", affected_nodes.len());
5219
5220 affected_nodes
5221 }
5222
5223 pub fn apply_text_changeset(&mut self) -> Vec<azul_core::dom::DomNodeId> {
5232 println!("[apply_text_changeset] Starting...");
5233
5234 let changeset = match self.text_input_manager.get_pending_changeset() {
5236 Some(cs) => {
5237 println!("[apply_text_changeset] Got changeset for node {:?}, inserted='{}', old_len={}",
5238 cs.node, cs.inserted_text.as_str(), cs.old_text.as_str().len());
5239 cs.clone()
5240 },
5241 None => {
5242 println!("[apply_text_changeset] ERROR: No pending changeset!");
5243 return Vec::new();
5244 }
5245 };
5246
5247 let node_id = match changeset.node.node.into_crate_internal() {
5248 Some(id) => {
5249 println!("[apply_text_changeset] Node ID: {:?}", id);
5250 id
5251 },
5252 None => {
5253 println!("[apply_text_changeset] ERROR: Invalid node ID");
5254 self.text_input_manager.clear_changeset();
5255 return Vec::new();
5256 }
5257 };
5258
5259 let dom_id = changeset.node.dom;
5260 println!("[apply_text_changeset] DOM ID: {:?}", dom_id);
5261
5262 let layout_result = match self.layout_results.get(&dom_id) {
5264 Some(lr) => lr,
5265 None => {
5266 println!("[apply_text_changeset] ERROR: No layout result for DOM {:?}", dom_id);
5267 self.text_input_manager.clear_changeset();
5268 return Vec::new();
5269 }
5270 };
5271
5272 let styled_node = match layout_result
5273 .styled_dom
5274 .node_data
5275 .as_ref()
5276 .get(node_id.index())
5277 {
5278 Some(node) => node,
5279 None => {
5280 println!("[apply_text_changeset] ERROR: No styled node at index {}", node_id.index());
5281 self.text_input_manager.clear_changeset();
5282 return Vec::new();
5283 }
5284 };
5285
5286 let is_contenteditable = styled_node.contenteditable
5290 || styled_node.attributes.as_ref().iter().any(|attr| {
5291 matches!(attr, azul_core::dom::AttributeType::ContentEditable(_))
5292 });
5293
5294 if !is_contenteditable {
5295 self.text_input_manager.clear_changeset();
5296 return Vec::new();
5297 }
5298
5299 let content = self.get_text_before_textinput(dom_id, node_id);
5301 println!("[apply_text_changeset] Got content, {} inline items", content.len());
5302
5303 let current_selection = if let Some(cursor) = self.cursor_manager.get_cursor() {
5305 println!("[apply_text_changeset] Cursor: run={}, byte={}",
5306 cursor.cluster_id.source_run, cursor.cluster_id.start_byte_in_run);
5307 vec![Selection::Cursor(cursor.clone())]
5308 } else {
5309 println!("[apply_text_changeset] No cursor, creating at position 0");
5310 vec![Selection::Cursor(TextCursor {
5312 cluster_id: GraphemeClusterId {
5313 source_run: 0,
5314 start_byte_in_run: 0,
5315 },
5316 affinity: CursorAffinity::Leading,
5317 })]
5318 };
5319
5320 let old_text = self.extract_text_from_inline_content(&content);
5322 let old_cursor = current_selection.first().and_then(|sel| {
5323 if let Selection::Cursor(c) = sel {
5324 Some(c.clone())
5325 } else {
5326 None
5327 }
5328 });
5329 let old_selection_range = current_selection.first().and_then(|sel| {
5330 if let Selection::Range(r) = sel {
5331 Some(*r)
5332 } else {
5333 None
5334 }
5335 });
5336
5337 let pre_state = crate::managers::undo_redo::NodeStateSnapshot {
5338 node_id: azul_core::id::NodeId::new(node_id.index()),
5339 text_content: old_text.into(),
5340 cursor_position: old_cursor.into(),
5341 selection_range: old_selection_range.into(),
5342 timestamp: azul_core::task::Instant::System(std::time::Instant::now().into()),
5343 };
5344
5345 use crate::text3::edit::{edit_text, TextEdit};
5347 let text_edit = TextEdit::Insert(changeset.inserted_text.as_str().to_string());
5348 println!("[apply_text_changeset] Calling edit_text() with Insert('{}')", changeset.inserted_text.as_str());
5349 let (new_content, new_selections) = edit_text(&content, ¤t_selection, &text_edit);
5350 println!("[apply_text_changeset] edit_text returned {} inline items, {} selections",
5351 new_content.len(), new_selections.len());
5352
5353 if let Some(Selection::Cursor(new_cursor)) = new_selections.first() {
5356 println!("[apply_text_changeset] Updating cursor to run={}, byte={}",
5357 new_cursor.cluster_id.source_run, new_cursor.cluster_id.start_byte_in_run);
5358 self.cursor_manager
5359 .move_cursor_to(new_cursor.clone(), dom_id, node_id);
5360 }
5361
5362 println!("[apply_text_changeset] Calling update_text_cache_after_edit()");
5364 self.update_text_cache_after_edit(dom_id, node_id, new_content);
5365 println!("[apply_text_changeset] Text cache updated successfully");
5366
5367 use crate::managers::changeset::{TextChangeset, TextOpInsertText, TextOperation};
5370
5371 let new_cursor = new_selections
5373 .first()
5374 .and_then(|sel| {
5375 if let Selection::Cursor(c) = sel {
5376 Some(CursorPosition::InWindow(
5380 azul_core::geom::LogicalPosition::new(0.0, 0.0),
5381 ))
5382 } else {
5383 None
5384 }
5385 })
5386 .unwrap_or(CursorPosition::Uninitialized);
5387
5388 let old_cursor_pos = old_cursor
5389 .as_ref()
5390 .map(|_| {
5391 CursorPosition::InWindow(azul_core::geom::LogicalPosition::new(0.0, 0.0))
5393 })
5394 .unwrap_or(CursorPosition::Uninitialized);
5395
5396 static CHANGESET_COUNTER: std::sync::atomic::AtomicUsize =
5398 std::sync::atomic::AtomicUsize::new(0);
5399 let changeset_id = CHANGESET_COUNTER.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
5400
5401 let undo_changeset = TextChangeset {
5402 id: changeset_id,
5403 target: changeset.node,
5404 operation: TextOperation::InsertText(TextOpInsertText {
5405 text: changeset.inserted_text.clone(),
5406 position: old_cursor_pos,
5407 new_cursor,
5408 }),
5409 timestamp: azul_core::task::Instant::System(std::time::Instant::now().into()),
5410 };
5411 self.undo_redo_manager
5412 .record_operation(undo_changeset, pre_state);
5413
5414 self.text_input_manager.clear_changeset();
5416 println!("[apply_text_changeset] Changeset cleared");
5417
5418 let dirty_nodes = self.determine_dirty_text_nodes(dom_id, node_id);
5420 println!("[apply_text_changeset] Dirty nodes: {:?}", dirty_nodes);
5421 dirty_nodes
5422 }
5423
5424 fn determine_dirty_text_nodes(
5428 &self,
5429 dom_id: DomId,
5430 node_id: NodeId,
5431 ) -> Vec<azul_core::dom::DomNodeId> {
5432 let layout_result = match self.layout_results.get(&dom_id) {
5433 Some(lr) => lr,
5434 None => return Vec::new(),
5435 };
5436
5437 let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(node_id));
5438 let node_dom_id = azul_core::dom::DomNodeId {
5439 dom: dom_id,
5440 node: hierarchy_id,
5441 };
5442
5443 let parent_id = layout_result
5445 .styled_dom
5446 .node_hierarchy
5447 .as_container()
5448 .get(node_id)
5449 .and_then(|item| item.parent_id())
5450 .map(|parent_node_id| {
5451 let parent_hierarchy_id =
5452 NodeHierarchyItemId::from_crate_internal(Some(parent_node_id));
5453 azul_core::dom::DomNodeId {
5454 dom: dom_id,
5455 node: parent_hierarchy_id,
5456 }
5457 });
5458
5459 if let Some(parent) = parent_id {
5461 vec![node_dom_id, parent]
5462 } else {
5463 vec![node_dom_id]
5464 }
5465 }
5466
5467 #[inline]
5469 pub fn process_text_input(
5470 &mut self,
5471 text_input: &str,
5472 ) -> BTreeMap<azul_core::dom::DomNodeId, (Vec<azul_core::events::EventFilter>, bool)> {
5473 println!("[process_text_input] Called with text: '{}'", text_input);
5474 let result = self.record_text_input(text_input);
5475 println!("[process_text_input] record_text_input returned {} affected nodes", result.len());
5476 for (node, (filters, has_text)) in &result {
5477 println!("[process_text_input] Node {:?}: {} filters, has_text={}", node, filters.len(), has_text);
5478 }
5479 result
5480 }
5481
5482 pub fn get_last_text_changeset(&self) -> Option<&PendingTextEdit> {
5484 self.text_input_manager.get_pending_changeset()
5485 }
5486
5487 pub fn get_text_before_textinput(&self, dom_id: DomId, node_id: NodeId) -> Vec<InlineContent> {
5497 if let Some(dirty_node) = self.dirty_text_nodes.get(&(dom_id, node_id)) {
5503 #[cfg(feature = "std")]
5504 eprintln!("[get_text_before_textinput] Using dirty_text_nodes content for ({:?}, {:?})", dom_id, node_id);
5505 return dirty_node.content.clone();
5506 }
5507
5508 let layout_result = match self.layout_results.get(&dom_id) {
5511 Some(lr) => lr,
5512 None => return Vec::new(),
5513 };
5514
5515 let node_data = match layout_result
5517 .styled_dom
5518 .node_data
5519 .as_ref()
5520 .get(node_id.index())
5521 {
5522 Some(nd) => nd,
5523 None => return Vec::new(),
5524 };
5525
5526 match node_data.get_node_type() {
5528 NodeType::Text(text) => {
5529 let style = self.get_text_style_for_node(dom_id, node_id);
5531
5532 vec![InlineContent::Text(StyledRun {
5533 text: text.as_str().to_string(),
5534 style,
5535 logical_start_byte: 0,
5536 source_node_id: Some(node_id),
5537 })]
5538 }
5539 NodeType::Div | NodeType::Body | NodeType::IFrame(_) => {
5540 self.collect_text_from_children(dom_id, node_id)
5542 }
5543 _ => {
5544 Vec::new()
5546 }
5547 }
5548 }
5549
5550 fn get_text_style_for_node(
5552 &self,
5553 dom_id: DomId,
5554 node_id: NodeId,
5555 ) -> alloc::sync::Arc<StyleProperties> {
5556 use alloc::sync::Arc;
5557
5558 let layout_result = match self.layout_results.get(&dom_id) {
5559 Some(lr) => lr,
5560 None => return Arc::new(Default::default()),
5561 };
5562
5563 let styled_nodes = layout_result.styled_dom.styled_nodes.as_ref();
5565 let _styled_node = match styled_nodes.get(node_id.index()) {
5566 Some(sn) => sn,
5567 None => return Arc::new(Default::default()),
5568 };
5569
5570 Arc::new(Default::default())
5574 }
5575
5576 fn collect_text_from_children(
5578 &self,
5579 dom_id: DomId,
5580 parent_node_id: NodeId,
5581 ) -> Vec<InlineContent> {
5582 let layout_result = match self.layout_results.get(&dom_id) {
5583 Some(lr) => lr,
5584 None => return Vec::new(),
5585 };
5586
5587 let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_ref();
5588 let parent_item = match node_hierarchy.get(parent_node_id.index()) {
5589 Some(item) => item,
5590 None => return Vec::new(),
5591 };
5592
5593 let mut result = Vec::new();
5594
5595 let mut current_child = parent_item.first_child_id(parent_node_id);
5597 while let Some(child_id) = current_child {
5598 let child_content = self.get_text_before_textinput(dom_id, child_id);
5600 result.extend(child_content);
5601
5602 let child_item = match node_hierarchy.get(child_id.index()) {
5604 Some(item) => item,
5605 None => break,
5606 };
5607 current_child = child_item.next_sibling_id();
5608 }
5609
5610 result
5611 }
5612
5613 pub fn extract_text_from_inline_content(&self, content: &[InlineContent]) -> String {
5617 let mut result = String::new();
5618
5619 for item in content {
5620 match item {
5621 InlineContent::Text(text_run) => {
5622 result.push_str(&text_run.text);
5623 }
5624 InlineContent::Space(_) => {
5625 result.push(' ');
5626 }
5627 InlineContent::LineBreak(_) => {
5628 result.push('\n');
5629 }
5630 InlineContent::Tab => {
5631 result.push('\t');
5632 }
5633 InlineContent::Ruby { base, .. } => {
5634 result.push_str(&self.extract_text_from_inline_content(base));
5636 }
5637 InlineContent::Marker { run, .. } => {
5638 result.push_str(&run.text);
5640 }
5641 InlineContent::Image(_) | InlineContent::Shape(_) => {}
5643 }
5644 }
5645
5646 result
5647 }
5648
5649 pub fn update_text_cache_after_edit(
5659 &mut self,
5660 dom_id: DomId,
5661 node_id: NodeId,
5662 new_inline_content: Vec<InlineContent>,
5663 ) {
5664 use crate::solver3::layout_tree::CachedInlineLayout;
5665
5666 println!("[update_text_cache_after_edit] Starting for DOM {:?}, node {:?}", dom_id, node_id);
5667 println!("[update_text_cache_after_edit] New content has {} inline items", new_inline_content.len());
5668 for (i, item) in new_inline_content.iter().enumerate() {
5669 match item {
5670 crate::text3::cache::InlineContent::Text(run) => {
5671 println!("[update_text_cache_after_edit] Item {}: Text('{}')", i, run.text);
5672 }
5673 _ => {
5674 println!("[update_text_cache_after_edit] Item {}: Non-text", i);
5675 }
5676 }
5677 }
5678
5679 let cursor = self.cursor_manager.get_cursor().cloned();
5681 self.dirty_text_nodes.insert(
5682 (dom_id, node_id),
5683 DirtyTextNode {
5684 content: new_inline_content.clone(),
5685 cursor,
5686 needs_ancestor_relayout: false, },
5688 );
5689 println!("[update_text_cache_after_edit] Stored in dirty_text_nodes");
5690
5691 let constraints = {
5694 let layout_result = match self.layout_results.get(&dom_id) {
5695 Some(r) => r,
5696 None => {
5697 println!("[update_text_cache_after_edit] ERROR: No layout result for DOM");
5698 return;
5699 }
5700 };
5701
5702 let layout_node = match layout_result.layout_tree.get(node_id.index()) {
5703 Some(n) => n,
5704 None => {
5705 println!("[update_text_cache_after_edit] ERROR: Node {} not found in layout tree", node_id.index());
5706 return;
5707 }
5708 };
5709
5710 let cached_layout = match &layout_node.inline_layout_result {
5711 Some(c) => c,
5712 None => {
5713 println!("[update_text_cache_after_edit] ERROR: No inline layout cached for node");
5714 return;
5715 }
5716 };
5717
5718 match &cached_layout.constraints {
5719 Some(c) => {
5720 println!("[update_text_cache_after_edit] Got cached constraints");
5721 c.clone()
5722 },
5723 None => {
5724 println!("[update_text_cache_after_edit] ERROR: No constraints cached");
5725 return;
5726 }
5727 }
5728 };
5729
5730 println!("[update_text_cache_after_edit] Re-running text3 layout pipeline...");
5732 let new_layout = self.relayout_text_node_internal(&new_inline_content, &constraints);
5733
5734 let Some(new_layout) = new_layout else {
5735 println!("[update_text_cache_after_edit] ERROR: relayout_text_node_internal returned None");
5736 return;
5737 };
5738 println!("[update_text_cache_after_edit] Text3 layout complete, {} items", new_layout.items.len());
5739
5740 if let Some(layout_result) = self.layout_results.get_mut(&dom_id) {
5743 if let Some(layout_node) = layout_result.layout_tree.get_mut(node_id.index()) {
5745 let old_size = layout_node.used_size;
5747 let new_bounds = new_layout.bounds();
5748 let new_size = Some(LogicalSize {
5749 width: new_bounds.width,
5750 height: new_bounds.height,
5751 });
5752 println!("[update_text_cache_after_edit] Old size: {:?}, new size: {:?}", old_size, new_size);
5753
5754 if let (Some(old), Some(new)) = (old_size, new_size) {
5756 if (old.height - new.height).abs() > 0.5 || (old.width - new.width).abs() > 0.5 {
5757 println!("[update_text_cache_after_edit] Size changed, marking for ancestor relayout");
5759 if let Some(dirty_node) = self.dirty_text_nodes.get_mut(&(dom_id, node_id)) {
5760 dirty_node.needs_ancestor_relayout = true;
5761 }
5762 }
5763 }
5764
5765 layout_node.inline_layout_result = Some(CachedInlineLayout::new_with_constraints(
5767 Arc::new(new_layout),
5768 constraints.available_width,
5769 false, constraints,
5771 ));
5772 println!("[update_text_cache_after_edit] Layout cache updated successfully");
5773 } else {
5774 println!("[update_text_cache_after_edit] ERROR: Layout node {} not found for update", node_id.index());
5775 }
5776 } else {
5777 println!("[update_text_cache_after_edit] ERROR: Layout result not found for update");
5778 }
5779 }
5780
5781 fn relayout_text_node_internal(
5783 &self,
5784 content: &[InlineContent],
5785 constraints: &UnifiedConstraints,
5786 ) -> Option<UnifiedLayout> {
5787 use crate::text3::cache::{
5788 create_logical_items, perform_fragment_layout, reorder_logical_items,
5789 shape_visual_items, BidiDirection, BreakCursor,
5790 };
5791
5792 let logical_items = create_logical_items(content, &[], &mut None);
5794
5795 if logical_items.is_empty() {
5796 return Some(UnifiedLayout {
5798 items: Vec::new(),
5799 overflow: crate::text3::cache::OverflowInfo::default(),
5800 });
5801 }
5802
5803 let base_direction = constraints.direction.unwrap_or(BidiDirection::Ltr);
5805 let visual_items = reorder_logical_items(&logical_items, base_direction, &mut None).ok()?;
5806
5807 let loaded_fonts = self.font_manager.get_loaded_fonts();
5809 let shaped_items = shape_visual_items(
5810 &visual_items,
5811 self.font_manager.get_font_chain_cache(),
5812 &self.font_manager.fc_cache,
5813 &loaded_fonts,
5814 &mut None,
5815 )
5816 .ok()?;
5817
5818 let mut cursor = BreakCursor::new(&shaped_items);
5820 perform_fragment_layout(&mut cursor, &logical_items, constraints, &mut None, &loaded_fonts)
5821 .ok()
5822 }
5823
5824 #[cfg(feature = "a11y")]
5826 fn get_node_used_size_a11y(
5827 &self,
5828 dom_id: DomId,
5829 node_id: NodeId,
5830 ) -> Option<azul_core::geom::LogicalSize> {
5831 let layout_result = self.layout_results.get(&dom_id)?;
5832 let node = layout_result.layout_tree.get(node_id.index())?;
5833 node.used_size
5834 }
5835
5836 fn get_node_bounds(
5838 &self,
5839 dom_id: DomId,
5840 node_id: NodeId,
5841 ) -> Option<azul_css::props::basic::LayoutRect> {
5842 use azul_css::props::basic::LayoutRect;
5843
5844 let layout_result = self.layout_results.get(&dom_id)?;
5845 let node = layout_result.layout_tree.get(node_id.index())?;
5846
5847 let size = node.used_size?;
5849
5850 let position = layout_result.calculated_positions.get(&node_id.index())?;
5852
5853 Some(LayoutRect {
5854 origin: azul_css::props::basic::LayoutPoint {
5855 x: position.x as f32 as isize,
5856 y: position.y as f32 as isize,
5857 },
5858 size: azul_css::props::basic::LayoutSize {
5859 width: size.width as isize,
5860 height: size.height as isize,
5861 },
5862 })
5863 }
5864
5865 #[cfg(feature = "a11y")]
5867 fn scroll_to_node_if_needed(
5868 &mut self,
5869 dom_id: DomId,
5870 node_id: NodeId,
5871 now: std::time::Instant,
5872 ) {
5873 if self.get_node_bounds(dom_id, node_id).is_some() {
5881 self.scroll_manager.scroll_to(
5882 dom_id,
5883 node_id,
5884 LogicalPosition { x: 0.0, y: 0.0 },
5885 std::time::Duration::from_millis(300).into(),
5886 azul_core::events::EasingFunction::EaseOut,
5887 now.into(),
5888 );
5889 }
5890 }
5891
5892 fn scroll_cursor_into_view_if_needed(
5905 &mut self,
5906 dom_id: DomId,
5907 node_id: NodeId,
5908 now: std::time::Instant,
5909 ) {
5910 let Some(cursor) = self.cursor_manager.get_cursor() else {
5912 return;
5913 };
5914
5915 let Some(inline_layout) = self.get_node_inline_layout(dom_id, node_id) else {
5917 return;
5918 };
5919
5920 let Some(cursor_rect) = inline_layout.get_cursor_rect(cursor) else {
5922 return;
5923 };
5924
5925 let Some(node_bounds) = self.get_node_bounds(dom_id, node_id) else {
5927 return;
5928 };
5929
5930 let cursor_abs_x = node_bounds.origin.x as f32 + cursor_rect.origin.x;
5932 let cursor_abs_y = node_bounds.origin.y as f32 + cursor_rect.origin.y;
5933
5934 let current_scroll = self
5940 .scroll_manager
5941 .get_current_offset(dom_id, node_id)
5942 .unwrap_or_default();
5943
5944 let viewport_x = node_bounds.origin.x as f32 + current_scroll.x;
5946 let viewport_y = node_bounds.origin.y as f32 + current_scroll.y;
5947 let viewport_width = node_bounds.size.width as f32;
5948 let viewport_height = node_bounds.size.height as f32;
5949
5950 let cursor_visible_x = (cursor_abs_x as f32) >= viewport_x
5952 && (cursor_abs_x as f32) <= viewport_x + viewport_width;
5953 let cursor_visible_y = (cursor_abs_y as f32) >= viewport_y
5954 && (cursor_abs_y as f32) <= viewport_y + viewport_height;
5955
5956 if cursor_visible_x && cursor_visible_y {
5957 return;
5959 }
5960
5961 let mut target_scroll_x = current_scroll.x;
5963 let mut target_scroll_y = current_scroll.y;
5964
5965 if (cursor_abs_x as f32) < viewport_x {
5967 target_scroll_x = cursor_abs_x as f32 - node_bounds.origin.x as f32;
5969 } else if (cursor_abs_x as f32) > viewport_x + viewport_width {
5970 target_scroll_x = cursor_abs_x as f32 - node_bounds.origin.x as f32 - viewport_width
5972 + cursor_rect.size.width;
5973 }
5974
5975 if (cursor_abs_y as f32) < viewport_y {
5977 target_scroll_y = cursor_abs_y as f32 - node_bounds.origin.y as f32;
5979 } else if (cursor_abs_y as f32) > viewport_y + viewport_height {
5980 target_scroll_y = cursor_abs_y as f32 - node_bounds.origin.y as f32 - viewport_height
5982 + cursor_rect.size.height;
5983 }
5984
5985 self.scroll_manager.scroll_to(
5987 dom_id,
5988 node_id,
5989 LogicalPosition {
5990 x: target_scroll_x,
5991 y: target_scroll_y,
5992 },
5993 std::time::Duration::from_millis(200).into(),
5994 azul_core::events::EasingFunction::EaseOut,
5995 now.into(),
5996 );
5997 }
5998
5999 fn byte_offset_to_cursor(
6014 &self,
6015 text_layout: &UnifiedLayout,
6016 byte_offset: u32,
6017 ) -> Option<TextCursor> {
6018 if byte_offset == 0 {
6020 for item in &text_layout.items {
6022 if let ShapedItem::Cluster(cluster) = &item.item {
6023 return Some(TextCursor {
6024 cluster_id: cluster.source_cluster_id,
6025 affinity: CursorAffinity::Trailing,
6026 });
6027 }
6028 }
6029 return Some(TextCursor {
6031 cluster_id: GraphemeClusterId {
6032 source_run: 0,
6033 start_byte_in_run: 0,
6034 },
6035 affinity: CursorAffinity::Trailing,
6036 });
6037 }
6038
6039 let mut current_byte_offset = 0u32;
6041
6042 for item in &text_layout.items {
6043 if let ShapedItem::Cluster(cluster) = &item.item {
6044 let cluster_byte_length = cluster.text.len() as u32;
6046 let cluster_end_byte = current_byte_offset + cluster_byte_length;
6047
6048 if byte_offset >= current_byte_offset && byte_offset <= cluster_end_byte {
6050 return Some(TextCursor {
6052 cluster_id: cluster.source_cluster_id,
6053 affinity: CursorAffinity::Trailing,
6054 });
6055 }
6056
6057 current_byte_offset = cluster_end_byte;
6058 }
6059 }
6060
6061 for item in text_layout.items.iter().rev() {
6063 if let ShapedItem::Cluster(cluster) = &item.item {
6064 return Some(TextCursor {
6065 cluster_id: cluster.source_cluster_id,
6066 affinity: CursorAffinity::Trailing,
6067 });
6068 }
6069 }
6070
6071 Some(TextCursor {
6073 cluster_id: GraphemeClusterId {
6074 source_run: 0,
6075 start_byte_in_run: 0,
6076 },
6077 affinity: CursorAffinity::Trailing,
6078 })
6079 }
6080
6081 fn get_node_inline_layout(
6086 &self,
6087 dom_id: DomId,
6088 node_id: NodeId,
6089 ) -> Option<alloc::sync::Arc<UnifiedLayout>> {
6090 let layout_tree = self.layout_cache.tree.as_ref()?;
6092
6093 let layout_node = layout_tree
6095 .nodes
6096 .iter()
6097 .find(|node| node.dom_node_id == Some(node_id))?;
6098
6099 layout_node
6101 .inline_layout_result
6102 .as_ref()
6103 .map(|c| c.clone_layout())
6104 }
6105
6106 pub fn sync_cursor_to_selection_manager(&mut self) {
6114 if let Some(cursor) = self.cursor_manager.get_cursor() {
6115 if let Some(location) = self.cursor_manager.get_cursor_location() {
6116 let selection = Selection::Cursor(cursor.clone());
6118
6119 let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(location.node_id));
6121 let dom_node_id = DomNodeId {
6122 dom: location.dom_id,
6123 node: hierarchy_id,
6124 };
6125
6126 let selection_state = SelectionState {
6127 selections: vec![selection].into(),
6128 node_id: dom_node_id,
6129 };
6130
6131 self.selection_manager
6133 .set_selection(location.dom_id, selection_state);
6134 }
6135 }
6136 }
6140
6141 #[must_use = "Returned nodes must be marked dirty for re-layout"]
6157 #[cfg(feature = "a11y")]
6158 pub fn edit_text_node(
6159 &mut self,
6160 dom_id: DomId,
6161 node_id: NodeId,
6162 edit_type: TextEditType,
6163 ) -> Vec<azul_core::dom::DomNodeId> {
6164 use crate::managers::text_input::TextInputSource;
6165
6166 let text_input = match &edit_type {
6168 TextEditType::ReplaceSelection(text) => text.clone(),
6169 TextEditType::SetValue(text) => text.clone(),
6170 TextEditType::SetNumericValue(value) => value.to_string(),
6171 };
6172
6173 let old_inline_content = self.get_text_before_textinput(dom_id, node_id);
6175 let old_text = self.extract_text_from_inline_content(&old_inline_content);
6176
6177 let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(node_id));
6179 let dom_node_id = azul_core::dom::DomNodeId {
6180 dom: dom_id,
6181 node: hierarchy_id,
6182 };
6183
6184 self.text_input_manager.record_input(
6186 dom_node_id,
6187 text_input,
6188 old_text,
6189 TextInputSource::Accessibility, );
6191
6192 self.apply_text_changeset()
6194 }
6195
6196 #[cfg(not(feature = "a11y"))]
6197 pub fn process_accessibility_action(
6198 &mut self,
6199 _dom_id: DomId,
6200 _node_id: NodeId,
6201 _action: azul_core::dom::AccessibilityAction,
6202 _now: std::time::Instant,
6203 ) -> BTreeMap<DomNodeId, (Vec<azul_core::events::EventFilter>, bool)> {
6204 BTreeMap::new()
6206 }
6207
6208 pub fn process_mouse_click_for_selection(
6236 &mut self,
6237 position: azul_core::geom::LogicalPosition,
6238 time_ms: u64,
6239 ) -> Option<Vec<azul_core::dom::DomNodeId>> {
6240 use crate::managers::hover::InputPointId;
6241 use crate::text3::selection::{select_paragraph_at_cursor, select_word_at_cursor};
6242
6243 #[cfg(feature = "std")]
6244 eprintln!("[DEBUG] process_mouse_click_for_selection: position=({:.1},{:.1}), time_ms={}",
6245 position.x, position.y, time_ms);
6246
6247 let mut found_selection: Option<(DomId, NodeId, SelectionRange, azul_core::geom::LogicalPosition)> = None;
6251
6252 if let Some(hit_test) = self.hover_manager.get_current(&InputPointId::Mouse) {
6254 #[cfg(feature = "std")]
6255 eprintln!("[DEBUG] HoverManager has hit test with {} doms", hit_test.hovered_nodes.len());
6256
6257 for (dom_id, hit) in &hit_test.hovered_nodes {
6259 let layout_result = match self.layout_results.get(dom_id) {
6260 Some(lr) => lr,
6261 None => continue,
6262 };
6263 let tree = &layout_result.layout_tree;
6265
6266 let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
6270 let get_dom_depth = |node_id: &NodeId| -> usize {
6271 let mut depth = 0;
6272 let mut current = *node_id;
6273 while let Some(parent) = node_hierarchy.get(current).and_then(|h| h.parent_id()) {
6274 depth += 1;
6275 current = parent;
6276 }
6277 depth
6278 };
6279
6280 let mut sorted_hits: Vec<_> = hit.regular_hit_test_nodes.iter().collect();
6281 sorted_hits.sort_by(|(a_id, _), (b_id, _)| {
6282 let depth_a = get_dom_depth(a_id);
6283 let depth_b = get_dom_depth(b_id);
6284 depth_b.cmp(&depth_a).then_with(|| a_id.index().cmp(&b_id.index()))
6287 });
6288
6289 for (node_id, hit_item) in sorted_hits {
6290 if !self.is_text_selectable(&layout_result.styled_dom, *node_id) {
6292 continue;
6293 }
6294
6295 let layout_node_idx = tree.nodes.iter().position(|n| n.dom_node_id == Some(*node_id));
6297 let layout_node_idx = match layout_node_idx {
6298 Some(idx) => idx,
6299 None => continue,
6300 };
6301 let layout_node = &tree.nodes[layout_node_idx];
6302
6303 let (cached_layout, ifc_root_node_id) = if let Some(ref cached) = layout_node.inline_layout_result {
6306 (cached, *node_id)
6308 } else if let Some(ref membership) = layout_node.ifc_membership {
6309 match tree.nodes.get(membership.ifc_root_layout_index) {
6311 Some(ifc_root) => match (ifc_root.inline_layout_result.as_ref(), ifc_root.dom_node_id) {
6312 (Some(cached), Some(root_dom_id)) => (cached, root_dom_id),
6313 _ => continue,
6314 },
6315 None => continue,
6316 }
6317 } else {
6318 continue;
6320 };
6321
6322 let layout = &cached_layout.layout;
6323
6324 let local_pos = hit_item.point_relative_to_item;
6327
6328 if let Some(cursor) = layout.hittest_cursor(local_pos) {
6330 found_selection = Some((*dom_id, ifc_root_node_id, SelectionRange {
6332 start: cursor.clone(),
6333 end: cursor,
6334 }, local_pos));
6335 break;
6336 }
6337 }
6338
6339 if found_selection.is_some() {
6340 break;
6341 }
6342 }
6343 }
6344
6345 if found_selection.is_none() {
6348 #[cfg(feature = "std")]
6349 eprintln!("[DEBUG] Fallback path: layout_results count = {}", self.layout_results.len());
6350
6351 for (dom_id, layout_result) in &self.layout_results {
6352 let tree = &layout_result.layout_tree;
6356
6357 #[cfg(feature = "std")]
6358 {
6359 let ifc_root_count = tree.nodes.iter()
6360 .filter(|n| n.inline_layout_result.is_some())
6361 .count();
6362 eprintln!("[DEBUG] DOM {:?}: tree has {} nodes, {} IFC roots",
6363 dom_id, tree.nodes.len(), ifc_root_count);
6364 }
6365
6366 for (node_idx, layout_node) in tree.nodes.iter().enumerate() {
6368 let cached_layout = match layout_node.inline_layout_result.as_ref() {
6369 Some(c) => c,
6370 None => continue, };
6372
6373 let node_id = match layout_node.dom_node_id {
6374 Some(n) => n,
6375 None => continue,
6376 };
6377
6378 if !self.is_text_selectable(&layout_result.styled_dom, node_id) {
6380 #[cfg(feature = "std")]
6381 eprintln!("[DEBUG] IFC root node_idx={} node_id={:?}: NOT selectable", node_idx, node_id);
6382 continue;
6383 }
6384
6385 let node_pos = layout_result.calculated_positions
6388 .get(&node_idx)
6389 .copied()
6390 .unwrap_or_default();
6391
6392 let node_size = layout_node.used_size.unwrap_or_else(|| {
6394 let bounds = cached_layout.layout.bounds();
6395 azul_core::geom::LogicalSize::new(bounds.width, bounds.height)
6396 });
6397
6398 #[cfg(feature = "std")]
6399 eprintln!("[DEBUG] IFC root node_idx={} node_id={:?}: pos=({:.1},{:.1}) size=({:.1},{:.1}), click=({:.1},{:.1})",
6400 node_idx, node_id, node_pos.x, node_pos.y, node_size.width, node_size.height, position.x, position.y);
6401
6402 if position.x < node_pos.x || position.x > node_pos.x + node_size.width ||
6403 position.y < node_pos.y || position.y > node_pos.y + node_size.height {
6404 #[cfg(feature = "std")]
6405 eprintln!("[DEBUG] -> OUT OF BOUNDS");
6406 continue;
6407 }
6408
6409 let local_pos = azul_core::geom::LogicalPosition {
6411 x: position.x - node_pos.x,
6412 y: position.y - node_pos.y,
6413 };
6414
6415 let layout = &cached_layout.layout;
6416
6417 if let Some(cursor) = layout.hittest_cursor(local_pos) {
6419 found_selection = Some((*dom_id, node_id, SelectionRange {
6420 start: cursor.clone(),
6421 end: cursor,
6422 }, local_pos));
6423 break;
6424 }
6425 }
6426
6427 if found_selection.is_some() {
6428 break;
6429 }
6430 }
6431 }
6432
6433 let (dom_id, ifc_root_node_id, initial_range, _local_pos) = found_selection?;
6434
6435 let node_hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(ifc_root_node_id));
6438 let dom_node_id = azul_core::dom::DomNodeId {
6439 dom: dom_id,
6440 node: node_hierarchy_id,
6441 };
6442
6443 let click_count = self
6445 .selection_manager
6446 .update_click_count(dom_node_id, position, time_ms);
6447
6448 let final_range = if click_count > 1 {
6450 let layout_result = self.layout_results.get(&dom_id)?;
6452 let tree = &layout_result.layout_tree;
6453
6454 let layout_node = tree.nodes.iter().find(|n| n.dom_node_id == Some(ifc_root_node_id))?;
6456 let cached_layout = layout_node.inline_layout_result.as_ref()?;
6457 let layout = &cached_layout.layout;
6458
6459 match click_count {
6460 2 => select_word_at_cursor(&initial_range.start, layout.as_ref())
6461 .unwrap_or(initial_range),
6462 3 => select_paragraph_at_cursor(&initial_range.start, layout.as_ref())
6463 .unwrap_or(initial_range),
6464 _ => initial_range,
6465 }
6466 } else {
6467 initial_range
6468 };
6469
6470 let char_bounds = {
6473 let layout_result = self.layout_results.get(&dom_id)?;
6474 let tree = &layout_result.layout_tree;
6475 let layout_node = tree.nodes.iter().find(|n| n.dom_node_id == Some(ifc_root_node_id))?;
6476 let cached_layout = layout_node.inline_layout_result.as_ref()?;
6477 cached_layout.layout.get_cursor_rect(&final_range.start)
6478 .unwrap_or(azul_core::geom::LogicalRect {
6479 origin: position,
6480 size: azul_core::geom::LogicalSize { width: 1.0, height: 16.0 },
6481 })
6482 };
6483
6484 self.selection_manager.clear_text_selection(&dom_id);
6486
6487 self.selection_manager.start_selection(
6489 dom_id,
6490 ifc_root_node_id,
6491 final_range.start,
6492 char_bounds,
6493 position,
6494 );
6495
6496 self.selection_manager.clear_selection(&dom_id);
6498
6499 let state = SelectionState {
6500 selections: vec![Selection::Range(final_range)].into(),
6501 node_id: dom_node_id,
6502 };
6503
6504 #[cfg(feature = "std")]
6505 eprintln!("[DEBUG] Setting selection on dom_id={:?}, node_id={:?}", dom_id, ifc_root_node_id);
6506
6507 self.selection_manager.set_selection(dom_id, state);
6508
6509 let is_contenteditable = self.layout_results.get(&dom_id)
6517 .map(|lr| {
6518 let node_hierarchy = lr.styled_dom.node_hierarchy.as_container();
6519 let node_data = lr.styled_dom.node_data.as_ref();
6520
6521 let mut current_node = Some(ifc_root_node_id);
6523 while let Some(node_id) = current_node {
6524 if let Some(styled_node) = node_data.get(node_id.index()) {
6525 if styled_node.contenteditable {
6529 #[cfg(feature = "std")]
6530 eprintln!("[DEBUG] Found contenteditable=true on node {:?}", node_id);
6531 return true;
6532 }
6533
6534 let has_contenteditable_attr = styled_node.attributes.as_ref().iter().any(|attr| {
6536 matches!(attr, azul_core::dom::AttributeType::ContentEditable(_))
6537 });
6538 if has_contenteditable_attr {
6539 #[cfg(feature = "std")]
6540 eprintln!("[DEBUG] Found contenteditable attribute on node {:?}", node_id);
6541 return true;
6542 }
6543 }
6544 current_node = node_hierarchy.get(node_id).and_then(|h| h.parent_id());
6546 }
6547 false
6548 })
6549 .unwrap_or(false);
6550
6551 if is_contenteditable {
6553 self.focus_manager.set_focused_node(Some(dom_node_id));
6554 #[cfg(feature = "std")]
6555 eprintln!("[DEBUG] Set focus on contenteditable node {:?}", ifc_root_node_id);
6556 }
6557
6558 let now = azul_core::task::Instant::now();
6562 self.cursor_manager.move_cursor_to(
6563 final_range.start.clone(),
6564 dom_id,
6565 ifc_root_node_id,
6566 );
6567 self.cursor_manager.reset_blink_on_input(now);
6569 self.cursor_manager.set_blink_timer_active(true);
6570
6571 #[cfg(feature = "std")]
6572 eprintln!("[DEBUG] Initialized cursor at {:?} for node {:?}", final_range.start, ifc_root_node_id);
6573
6574 Some(vec![dom_node_id])
6576 }
6577
6578 pub fn process_mouse_drag_for_selection(
6595 &mut self,
6596 _start_position: azul_core::geom::LogicalPosition,
6597 current_position: azul_core::geom::LogicalPosition,
6598 ) -> Option<Vec<azul_core::dom::DomNodeId>> {
6599 use crate::managers::hover::InputPointId;
6600
6601 #[cfg(feature = "std")]
6602 eprintln!("[DEBUG] process_mouse_drag_for_selection: current=({:.1},{:.1})",
6603 current_position.x, current_position.y);
6604
6605 let dom_id = self.selection_manager.get_all_text_selections()
6607 .keys()
6608 .next()
6609 .copied()?;
6610
6611 let anchor = {
6613 let text_selection = self.selection_manager.get_text_selection(&dom_id)?;
6614 text_selection.anchor.clone()
6615 };
6616
6617 #[cfg(feature = "std")]
6618 eprintln!("[DEBUG] Found anchor at IFC root {:?}, cursor {:?}",
6619 anchor.ifc_root_node_id, anchor.cursor.cluster_id);
6620
6621 let hit_test = self.hover_manager.get_current(&InputPointId::Mouse)?;
6623
6624 let mut focus_info: Option<(NodeId, TextCursor, azul_core::geom::LogicalPosition)> = None;
6626
6627 for (hit_dom_id, hit) in &hit_test.hovered_nodes {
6628 if *hit_dom_id != dom_id {
6629 continue;
6630 }
6631
6632 let layout_result = match self.layout_results.get(hit_dom_id) {
6633 Some(lr) => lr,
6634 None => continue,
6635 };
6636 let tree = &layout_result.layout_tree;
6637
6638 for (node_id, hit_item) in &hit.regular_hit_test_nodes {
6639 if !self.is_text_selectable(&layout_result.styled_dom, *node_id) {
6640 continue;
6641 }
6642
6643 let layout_node_idx = tree.nodes.iter().position(|n| n.dom_node_id == Some(*node_id));
6644 let layout_node_idx = match layout_node_idx {
6645 Some(idx) => idx,
6646 None => continue,
6647 };
6648 let layout_node = &tree.nodes[layout_node_idx];
6649
6650 let (cached_layout, ifc_root_node_id) = if let Some(ref cached) = layout_node.inline_layout_result {
6652 (cached, *node_id)
6653 } else if let Some(ref membership) = layout_node.ifc_membership {
6654 match tree.nodes.get(membership.ifc_root_layout_index) {
6655 Some(ifc_root) => match (ifc_root.inline_layout_result.as_ref(), ifc_root.dom_node_id) {
6656 (Some(cached), Some(root_dom_id)) => (cached, root_dom_id),
6657 _ => continue,
6658 },
6659 None => continue,
6660 }
6661 } else {
6662 continue;
6663 };
6664
6665 let local_pos = hit_item.point_relative_to_item;
6666
6667 if let Some(cursor) = cached_layout.layout.hittest_cursor(local_pos) {
6668 focus_info = Some((ifc_root_node_id, cursor, current_position));
6669 break;
6670 }
6671 }
6672
6673 if focus_info.is_some() {
6674 break;
6675 }
6676 }
6677
6678 let (focus_ifc_root, focus_cursor, focus_mouse_pos) = focus_info?;
6679
6680 #[cfg(feature = "std")]
6681 eprintln!("[DEBUG] Found focus at IFC root {:?}, cursor {:?}",
6682 focus_ifc_root, focus_cursor.cluster_id);
6683
6684 let layout_result = self.layout_results.get(&dom_id)?;
6686 let hierarchy = &layout_result.styled_dom.node_hierarchy;
6687
6688 let is_forward = if anchor.ifc_root_node_id == focus_ifc_root {
6690 anchor.cursor <= focus_cursor
6692 } else {
6693 is_before_in_document_order(hierarchy, anchor.ifc_root_node_id, focus_ifc_root)
6695 };
6696
6697 let (start_node, end_node) = if is_forward {
6698 (anchor.ifc_root_node_id, focus_ifc_root)
6699 } else {
6700 (focus_ifc_root, anchor.ifc_root_node_id)
6701 };
6702
6703 let nodes_in_range = collect_nodes_in_document_order(hierarchy, start_node, end_node);
6705
6706 #[cfg(feature = "std")]
6707 eprintln!("[DEBUG] Nodes in range: {:?}, is_forward: {}",
6708 nodes_in_range.iter().map(|n| n.index()).collect::<Vec<_>>(), is_forward);
6709
6710 let mut affected_nodes_map = std::collections::BTreeMap::new();
6712 let tree = &layout_result.layout_tree;
6713
6714 for node_id in &nodes_in_range {
6715 let layout_node = tree.nodes.iter().find(|n| n.dom_node_id == Some(*node_id));
6717 let layout_node = match layout_node {
6718 Some(ln) if ln.inline_layout_result.is_some() => ln,
6719 _ => continue, };
6721
6722 let cached_layout = layout_node.inline_layout_result.as_ref()?;
6723 let layout = &cached_layout.layout;
6724
6725 let range = if *node_id == anchor.ifc_root_node_id && *node_id == focus_ifc_root {
6726 SelectionRange {
6728 start: if is_forward { anchor.cursor } else { focus_cursor },
6729 end: if is_forward { focus_cursor } else { anchor.cursor },
6730 }
6731 } else if *node_id == anchor.ifc_root_node_id {
6732 if is_forward {
6734 let end_cursor = layout.get_last_cluster_cursor()
6735 .unwrap_or(anchor.cursor);
6736 SelectionRange { start: anchor.cursor, end: end_cursor }
6737 } else {
6738 let start_cursor = layout.get_first_cluster_cursor()
6739 .unwrap_or(anchor.cursor);
6740 SelectionRange { start: start_cursor, end: anchor.cursor }
6741 }
6742 } else if *node_id == focus_ifc_root {
6743 if is_forward {
6745 let start_cursor = layout.get_first_cluster_cursor()
6746 .unwrap_or(focus_cursor);
6747 SelectionRange { start: start_cursor, end: focus_cursor }
6748 } else {
6749 let end_cursor = layout.get_last_cluster_cursor()
6750 .unwrap_or(focus_cursor);
6751 SelectionRange { start: focus_cursor, end: end_cursor }
6752 }
6753 } else {
6754 let start_cursor = layout.get_first_cluster_cursor()?;
6756 let end_cursor = layout.get_last_cluster_cursor()?;
6757 SelectionRange { start: start_cursor, end: end_cursor }
6758 };
6759
6760 affected_nodes_map.insert(*node_id, range);
6761 }
6762
6763 #[cfg(feature = "std")]
6764 eprintln!("[DEBUG] Affected IFC roots: {:?}",
6765 affected_nodes_map.keys().map(|n| n.index()).collect::<Vec<_>>());
6766
6767 self.selection_manager.update_selection_focus(
6770 &dom_id,
6771 focus_ifc_root,
6772 focus_cursor,
6773 focus_mouse_pos,
6774 affected_nodes_map.clone(),
6775 is_forward,
6776 );
6777
6778 if let Some(anchor_range) = affected_nodes_map.get(&anchor.ifc_root_node_id) {
6781 let node_hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(anchor.ifc_root_node_id));
6782 let dom_node_id = azul_core::dom::DomNodeId {
6783 dom: dom_id,
6784 node: node_hierarchy_id,
6785 };
6786
6787 let state = SelectionState {
6788 selections: vec![Selection::Range(*anchor_range)].into(),
6789 node_id: dom_node_id,
6790 };
6791 self.selection_manager.set_selection(dom_id, state);
6792 }
6793
6794 let affected_dom_nodes: Vec<azul_core::dom::DomNodeId> = affected_nodes_map.keys()
6796 .map(|node_id| azul_core::dom::DomNodeId {
6797 dom: dom_id,
6798 node: NodeHierarchyItemId::from_crate_internal(Some(*node_id)),
6799 })
6800 .collect();
6801
6802 if affected_dom_nodes.is_empty() {
6803 None
6804 } else {
6805 Some(affected_dom_nodes)
6806 }
6807 }
6808
6809 pub fn delete_selection(
6822 &mut self,
6823 target: azul_core::dom::DomNodeId,
6824 forward: bool,
6825 ) -> Option<Vec<azul_core::dom::DomNodeId>> {
6826 let dom_id = target.dom;
6827
6828 let ranges = self.selection_manager.get_ranges(&dom_id);
6830 if ranges.is_empty() {
6831 return None; }
6833
6834 let mut earliest_cursor = None;
6841 for range in &ranges {
6842 let cursor = if forward { range.end } else { range.start };
6844
6845 if earliest_cursor.is_none() {
6846 earliest_cursor = Some(cursor);
6847 } else if let Some(current) = earliest_cursor {
6848 if cursor < current {
6851 earliest_cursor = Some(cursor);
6852 }
6853 }
6854 }
6855
6856 self.selection_manager.clear_selection(&dom_id);
6858
6859 if let Some(cursor) = earliest_cursor {
6860 let state = SelectionState {
6862 selections: vec![Selection::Range(SelectionRange {
6863 start: cursor.clone(),
6864 end: cursor,
6865 })]
6866 .into(),
6867 node_id: target,
6868 };
6869 self.selection_manager.set_selection(dom_id, state);
6870 }
6871
6872 Some(vec![target])
6874 }
6875
6876 pub fn get_selected_content_for_clipboard(
6892 &self,
6893 dom_id: &DomId,
6894 ) -> Option<crate::managers::selection::ClipboardContent> {
6895 use crate::{
6896 managers::selection::{ClipboardContent, StyledTextRun},
6897 text3::cache::ShapedItem,
6898 };
6899
6900 let ranges = self.selection_manager.get_ranges(dom_id);
6902 if ranges.is_empty() {
6903 return None;
6904 }
6905
6906 let mut plain_text = String::new();
6907 let mut styled_runs = Vec::new();
6908
6909 for cache_id in self.text_cache.get_all_layout_ids() {
6911 let layout = self.text_cache.get_layout(&cache_id)?;
6912
6913 for range in &ranges {
6915 for positioned_item in &layout.items {
6917 match &positioned_item.item {
6918 ShapedItem::Cluster(cluster) => {
6919 let cluster_id = cluster.source_cluster_id;
6921
6922 let in_range = if range.start.cluster_id <= range.end.cluster_id {
6924 cluster_id >= range.start.cluster_id
6925 && cluster_id <= range.end.cluster_id
6926 } else {
6927 cluster_id >= range.end.cluster_id
6928 && cluster_id <= range.start.cluster_id
6929 };
6930
6931 if in_range {
6932 plain_text.push_str(&cluster.text);
6934
6935 if let Some(first_glyph) = cluster.glyphs.first() {
6937 let style = &first_glyph.style;
6938
6939 let default_font = FontSelector::default();
6941 let first_font = style.font_stack.first_selector()
6942 .unwrap_or(&default_font);
6943 let font_family: OptionString =
6944 Some(AzString::from(first_font.family.as_str())).into();
6945
6946 use rust_fontconfig::FcWeight;
6948 let is_bold = matches!(
6949 first_font.weight,
6950 FcWeight::Bold | FcWeight::ExtraBold | FcWeight::Black
6951 );
6952 let is_italic = matches!(
6953 first_font.style,
6954 FontStyle::Italic | FontStyle::Oblique
6955 );
6956
6957 styled_runs.push(StyledTextRun {
6958 text: cluster.text.clone().into(),
6959 font_family,
6960 font_size_px: style.font_size_px,
6961 color: style.color,
6962 is_bold,
6963 is_italic,
6964 });
6965 }
6966 }
6967 }
6968 _ => {}
6970 }
6971 }
6972 }
6973 }
6974
6975 if plain_text.is_empty() {
6976 None
6977 } else {
6978 Some(ClipboardContent {
6979 plain_text: plain_text.into(),
6980 styled_runs: styled_runs.into(),
6981 })
6982 }
6983 }
6984
6985 pub fn process_image_callback_updates(
7000 &mut self,
7001 image_callbacks_changed: &BTreeMap<DomId, FastBTreeSet<NodeId>>,
7002 gl_context: &OptionGlContextPtr,
7003 ) -> Vec<(DomId, NodeId, azul_core::gl::Texture)> {
7004 use crate::callbacks::{RenderImageCallback, RenderImageCallbackInfo};
7005
7006 let mut updated_textures = Vec::new();
7007
7008 for (dom_id, node_ids) in image_callbacks_changed {
7009 let layout_result = match self.layout_results.get_mut(dom_id) {
7010 Some(lr) => lr,
7011 None => continue,
7012 };
7013
7014 for node_id in node_ids {
7015 let node_data_container = layout_result.styled_dom.node_data.as_container();
7017 let node_data = match node_data_container.get(*node_id) {
7018 Some(nd) => nd,
7019 None => continue,
7020 };
7021
7022 let has_callback = matches!(node_data.get_node_type(), NodeType::Image(img_ref)
7024 if img_ref.get_image_callback().is_some());
7025
7026 if !has_callback {
7027 continue;
7028 }
7029
7030 let layout_indices = match layout_result.layout_tree.dom_to_layout.get(node_id) {
7033 Some(indices) if !indices.is_empty() => indices,
7034 _ => continue,
7035 };
7036
7037 let layout_index = layout_indices[0];
7039
7040 let position = match layout_result.calculated_positions.get(&layout_index) {
7042 Some(pos) => *pos,
7043 None => continue,
7044 };
7045
7046 let layout_node = match layout_result.layout_tree.get(layout_index) {
7048 Some(ln) => ln,
7049 None => continue,
7050 };
7051
7052 let (width, height) = match layout_node.used_size {
7054 Some(size) => (size.width, size.height),
7055 None => continue, };
7057
7058 let callback_domnode_id = DomNodeId {
7059 dom: *dom_id,
7060 node: azul_core::styled_dom::NodeHierarchyItemId::from_crate_internal(Some(
7061 *node_id,
7062 )),
7063 };
7064
7065 let bounds = HidpiAdjustedBounds::from_bounds(
7066 azul_css::props::basic::LayoutSize {
7067 width: width as isize,
7068 height: height as isize,
7069 },
7070 self.current_window_state.size.get_hidpi_factor(),
7071 );
7072
7073 let mut gl_callback_info = RenderImageCallbackInfo::new(
7075 callback_domnode_id,
7076 bounds,
7077 gl_context,
7078 &self.image_cache,
7079 &self.font_manager.fc_cache,
7080 );
7081
7082 let new_image_ref = {
7084 let mut node_data_mut = layout_result.styled_dom.node_data.as_container_mut();
7085 match node_data_mut.get_mut(*node_id) {
7086 Some(nd) => {
7087 match &mut nd.node_type {
7088 NodeType::Image(img_ref) => {
7089 img_ref.get_image_callback_mut().map(|core_callback| {
7090 let callback =
7093 RenderImageCallback::from_core(&core_callback.callback);
7094 (callback.cb)(
7095 core_callback.refany.clone(),
7096 gl_callback_info,
7097 )
7098 })
7099 }
7100 _ => None,
7101 }
7102 }
7103 None => None,
7104 }
7105 };
7106
7107 #[cfg(feature = "gl_context_loader")]
7109 if let Some(gl) = gl_context.as_ref() {
7110 use gl_context_loader::gl;
7111 gl.bind_framebuffer(gl::FRAMEBUFFER, 0);
7112 gl.disable(gl::FRAMEBUFFER_SRGB);
7113 gl.disable(gl::MULTISAMPLE);
7114 }
7115
7116 if let Some(image_ref) = new_image_ref {
7118 if let Some(decoded_image) = image_ref.into_inner() {
7119 if let azul_core::resources::DecodedImage::Gl(texture) = decoded_image {
7120 updated_textures.push((*dom_id, *node_id, texture));
7121 }
7122 }
7123 }
7124 }
7125 }
7126
7127 updated_textures
7128 }
7129
7130 pub fn process_iframe_updates(
7147 &mut self,
7148 iframes_to_update: &BTreeMap<DomId, FastBTreeSet<NodeId>>,
7149 window_state: &FullWindowState,
7150 renderer_resources: &RendererResources,
7151 system_callbacks: &ExternalSystemCallbacks,
7152 ) -> Vec<(DomId, NodeId)> {
7153 let mut updated_iframes = Vec::new();
7154
7155 for (dom_id, node_ids) in iframes_to_update {
7156 for node_id in node_ids {
7157 let bounds = match Self::get_iframe_bounds_from_layout(
7159 &self.layout_results,
7160 *dom_id,
7161 *node_id,
7162 ) {
7163 Some(b) => b,
7164 None => continue,
7165 };
7166
7167 self.iframe_manager.force_reinvoke(*dom_id, *node_id);
7169
7170 if let Some(_child_dom_id) = self.invoke_iframe_callback(
7172 *dom_id,
7173 *node_id,
7174 bounds,
7175 window_state,
7176 renderer_resources,
7177 system_callbacks,
7178 &mut None,
7179 ) {
7180 updated_iframes.push((*dom_id, *node_id));
7181 }
7182 }
7183 }
7184
7185 updated_iframes
7186 }
7187
7188 pub fn queue_iframe_updates(
7192 &mut self,
7193 iframes_to_update: BTreeMap<DomId, FastBTreeSet<NodeId>>,
7194 ) {
7195 for (dom_id, node_ids) in iframes_to_update {
7196 self.pending_iframe_updates
7197 .entry(dom_id)
7198 .or_insert_with(FastBTreeSet::new)
7199 .extend(node_ids);
7200 }
7201 }
7202
7203 pub fn process_pending_iframe_updates(
7207 &mut self,
7208 window_state: &FullWindowState,
7209 renderer_resources: &RendererResources,
7210 system_callbacks: &ExternalSystemCallbacks,
7211 ) -> Vec<(DomId, NodeId)> {
7212 if self.pending_iframe_updates.is_empty() {
7213 return Vec::new();
7214 }
7215
7216 let iframes_to_update = core::mem::take(&mut self.pending_iframe_updates);
7218
7219 self.process_iframe_updates(
7221 &iframes_to_update,
7222 window_state,
7223 renderer_resources,
7224 system_callbacks,
7225 )
7226 }
7227
7228 fn get_iframe_bounds_from_layout(
7232 layout_results: &BTreeMap<DomId, DomLayoutResult>,
7233 dom_id: DomId,
7234 node_id: NodeId,
7235 ) -> Option<LogicalRect> {
7236 let layout_result = layout_results.get(&dom_id)?;
7237
7238 let node_data_container = layout_result.styled_dom.node_data.as_container();
7240 let node_data = node_data_container.get(node_id)?;
7241
7242 if !matches!(node_data.get_node_type(), NodeType::IFrame(_)) {
7243 return None;
7244 }
7245
7246 let layout_indices = layout_result.layout_tree.dom_to_layout.get(&node_id)?;
7248 if layout_indices.is_empty() {
7249 return None;
7250 }
7251
7252 let layout_index = layout_indices[0];
7253
7254 let position = *layout_result.calculated_positions.get(&layout_index)?;
7256
7257 let layout_node = layout_result.layout_tree.get(layout_index)?;
7259 let size = layout_node.used_size?;
7260
7261 Some(LogicalRect::new(
7262 position,
7263 LogicalSize::new(size.width as f32, size.height as f32),
7264 ))
7265 }
7266}
7267
7268#[cfg(feature = "a11y")]
7269#[derive(Debug, Clone)]
7270pub enum TextEditType {
7271 ReplaceSelection(String),
7272 SetValue(String),
7273 SetNumericValue(f64),
7274}