1use std::{
22 collections::{BTreeMap, BTreeSet, HashMap},
23 sync::{
24 atomic::{AtomicUsize, Ordering},
25 Arc,
26 },
27};
28
29use azul_core::{
30 animation::UpdateImageType,
31 callbacks::{FocusTarget, HidpiAdjustedBounds, VirtualViewCallbackReason, Update},
32 dom::{
33 AccessibilityAction, AttributeType, Dom, DomId, DomIdVec, DomNodeId, NodeId, NodeType, On,
34 },
35 events::{EasingFunction, EventFilter, FocusEventFilter, HoverEventFilter},
36 geom::{LogicalPosition, LogicalRect, LogicalSize, OptionLogicalPosition},
37 gl::OptionGlContextPtr,
38 gpu::{GpuScrollbarOpacityEvent, GpuValueCache},
39 hit_test::{DocumentId, ScrollPosition, ScrollbarHitId},
40 refany::{OptionRefAny, RefAny},
41 resources::{
42 Epoch, FontKey, GlTextureCache, IdNamespace, ImageCache, ImageMask, ImageRef, ImageRefHash,
43 OpacityKey, RendererResources,
44 },
45 selection::{
46 CursorAffinity, GraphemeClusterId, Selection, SelectionAnchor, SelectionFocus,
47 SelectionRange, SelectionState, TextCursor, TextSelection,
48 },
49 styled_dom::{
50 collect_nodes_in_document_order, is_before_in_document_order, NodeHierarchyItemId,
51 StyledDom,
52 },
53 task::{
54 Duration, Instant, SystemTickDiff, SystemTimeDiff, TerminateTimer, ThreadId, ThreadIdVec,
55 ThreadSendMsg, TimerId, TimerIdVec,
56 },
57 window::{CursorPosition, MonitorVec, RawWindowHandle, RendererType},
58 FastBTreeSet, OrderedMap,
59};
60use azul_css::{
61 css::Css,
62 props::{
63 basic::FontRef,
64 property::{CssProperty, CssPropertyVec},
65 },
66 AzString, LayoutDebugMessage, OptionString,
67};
68use rust_fontconfig::FcFontCache;
69
70#[cfg(feature = "icu")]
71use crate::icu::IcuLocalizerHandle;
72use crate::{
73 callbacks::{
74 Callback, ExternalSystemCallbacks, MenuCallback,
75 },
76 managers::{
77 gpu_state::GpuStateManager,
78 virtual_view::VirtualViewManager,
79 scroll_state::{ScrollManager, ScrollStates},
80 },
81 solver3::{
82 self, cache::LayoutCache as Solver3LayoutCache, display_list::DisplayList,
83 layout_tree::LayoutTree,
84 },
85 text3::{
86 cache::{
87 FontManager, FontSelector, FontStyle, InlineContent, TextShapingCache as TextLayoutCache,
88 LayoutError, ShapedItem, StyleProperties, StyledRun, TextBoundary, UnifiedConstraints,
89 UnifiedLayout,
90 },
91 default::PathLoader,
92 },
93 thread::{OptionThreadReceiveMsg, Thread, ThreadReceiveMsg, ThreadWriteBackMsg},
94 timer::Timer,
95 window_state::{FullWindowState, WindowCreateOptions},
96};
97
98static DOCUMENT_ID_COUNTER: AtomicUsize = AtomicUsize::new(0);
100static ID_NAMESPACE_COUNTER: AtomicUsize = AtomicUsize::new(0);
101
102fn new_document_id() -> DocumentId {
104 let namespace_id = new_id_namespace();
105 let id = DOCUMENT_ID_COUNTER.fetch_add(1, Ordering::Relaxed) as u32;
106 DocumentId { namespace_id, id }
107}
108
109#[derive(Debug, Copy, Clone, PartialEq, Eq)]
111pub enum CursorNavigationDirection {
112 Up,
114 Down,
116 Left,
118 Right,
120 LineStart,
122 LineEnd,
124 DocumentStart,
126 DocumentEnd,
128}
129
130#[derive(Debug, Clone)]
132pub enum CursorMovementResult {
133 MovedWithinNode(TextCursor),
135 MovedToNode {
137 dom_id: DomId,
138 node_id: NodeId,
139 cursor: TextCursor,
140 },
141 AtBoundary {
143 boundary: TextBoundary,
144 cursor: TextCursor,
145 },
146}
147
148#[derive(Debug, Clone)]
150pub struct NoCursorDestination {
151 pub reason: String,
152}
153
154#[derive(Debug, Clone)]
159pub enum CursorBlinkTimerAction {
160 Start(crate::timer::Timer),
162 Stop,
164 NoChange,
166}
167
168#[derive(Debug, Clone)]
172pub enum TooltipTimerAction {
173 Start(crate::timer::Timer),
175 Stop,
177 NoChange,
179}
180
181fn new_id_namespace() -> IdNamespace {
183 let id = ID_NAMESPACE_COUNTER.fetch_add(1, Ordering::Relaxed) as u32;
184 IdNamespace(id)
185}
186
187extern "C" fn cursor_blink_timer_destructor(_: RefAny) {
193 }
195
196pub extern "C" fn cursor_blink_timer_callback(
206 _data: RefAny,
207 mut info: crate::timer::TimerCallbackInfo,
208) -> azul_core::callbacks::TimerCallbackReturn {
209 use azul_core::callbacks::{TimerCallbackReturn, Update};
210 use azul_core::task::TerminateTimer;
211
212 let now = info.get_current_time();
214
215 info.set_cursor_visibility_toggle();
233
234 TimerCallbackReturn {
242 should_update: Update::DoNothing,
243 should_terminate: TerminateTimer::Continue,
244 }
245}
246
247pub extern "C" fn tooltip_delay_timer_callback(
261 _data: RefAny,
262 mut info: crate::timer::TimerCallbackInfo,
263) -> azul_core::callbacks::TimerCallbackReturn {
264 use azul_core::callbacks::{TimerCallbackReturn, Update};
265 use azul_core::task::TerminateTimer;
266
267 let layout_window = info.callback_info.get_layout_window();
268 let hover_node_id = layout_window
269 .hover_manager
270 .current_hover_node()
271 .map(|node_id| azul_core::dom::DomNodeId {
272 dom: azul_core::dom::DomId { inner: 0 },
273 node: azul_core::styled_dom::NodeHierarchyItemId::from_crate_internal(Some(node_id)),
274 });
275
276 if let Some(dom_node_id) = hover_node_id {
277 let tooltip_text = info
279 .callback_info
280 .get_node_attribute(dom_node_id, "aria-label")
281 .or_else(|| info.callback_info.get_node_attribute(dom_node_id, "alt"))
282 .or_else(|| info.callback_info.get_node_attribute(dom_node_id, "title"));
283
284 if let Some(text) = tooltip_text {
285 info.callback_info.show_tooltip(text);
286 }
287 }
288
289 TimerCallbackReturn {
290 should_update: Update::DoNothing,
291 should_terminate: TerminateTimer::Terminate,
292 }
293}
294
295#[derive(Debug)]
297pub struct DomLayoutResult {
298 pub styled_dom: StyledDom,
300 pub layout_tree: LayoutTree,
302 pub calculated_positions: crate::solver3::PositionVec,
304 pub viewport: LogicalRect,
306 pub display_list: DisplayList,
308 pub scroll_ids: HashMap<usize, u64>,
311 pub scroll_id_to_node_id: HashMap<u64, NodeId>,
314}
315
316#[derive(Debug, Clone)]
318pub struct ScrollbarDragState {
319 pub hit_id: ScrollbarHitId,
320 pub initial_mouse_pos: LogicalPosition,
321 pub initial_scroll_offset: LogicalPosition,
322}
323
324pub use crate::managers::text_input::PendingTextEdit;
328
329#[derive(Debug, Clone)]
332pub struct TextConstraintsCache {
333 pub constraints: BTreeMap<(DomId, NodeId), UnifiedConstraints>,
335}
336
337impl Default for TextConstraintsCache {
338 fn default() -> Self {
339 Self {
340 constraints: BTreeMap::new(),
341 }
342 }
343}
344
345#[derive(Debug, Clone)]
348pub struct DirtyTextNode {
349 pub content: Vec<InlineContent>,
351 pub cursor: Option<TextCursor>,
353 pub needs_ancestor_relayout: bool,
355}
356
357pub struct TextChangesetResult {
359 pub dirty_nodes: Vec<azul_core::dom::DomNodeId>,
361 pub needs_relayout: bool,
364}
365
366pub struct LayoutWindow {
375 pub skip_gpu_sync: bool,
383 #[cfg(feature = "pdf")]
385 pub fragmentation_context: crate::paged::FragmentationContext,
386 pub layout_cache: Solver3LayoutCache,
388 pub text_cache: TextLayoutCache,
390 pub font_manager: FontManager<FontRef>,
392 pub image_cache: ImageCache,
394 pub layout_results: BTreeMap<DomId, DomLayoutResult>,
396 pub scroll_manager: ScrollManager,
398 pub gesture_drag_manager: crate::managers::gesture::GestureAndDragManager,
400 pub focus_manager: crate::managers::focus_cursor::FocusManager,
402 pub text_edit_manager: crate::managers::text_edit::TextEditManager,
404 pub file_drop_manager: crate::managers::file_drop::FileDropManager,
406 pub clipboard_manager: crate::managers::clipboard::ClipboardManager,
408 pub drag_drop_manager: crate::managers::drag_drop::DragDropManager,
410 pub hover_manager: crate::managers::hover::HoverManager,
412 pub virtual_view_manager: VirtualViewManager,
414 pub gpu_state_manager: GpuStateManager,
416 pub a11y_manager: crate::managers::a11y::A11yManager,
418 pub permission_manager: crate::managers::permission::PermissionManager,
425 pub geolocation_manager: crate::managers::geolocation::GeolocationManager,
432 pub biometric_manager: crate::managers::biometric::BiometricManager,
437 pub keyring_manager: crate::managers::keyring::KeyringManager,
442 pub sensor_manager: crate::managers::sensors::SensorManager,
447 pub gamepad_manager: crate::managers::gamepad::GamepadManager,
451 pub safe_area_insets: azul_css::system::SafeAreaInsets,
455 pub timers: BTreeMap<TimerId, Timer>,
457 pub threads: BTreeMap<ThreadId, Thread>,
459 pub renderer_resources: RendererResources,
461 pub renderer_type: Option<RendererType>,
463 pub previous_window_state: Option<FullWindowState>,
465 pub current_window_state: FullWindowState,
468 pub document_id: DocumentId,
471 pub id_namespace: IdNamespace,
473 pub epoch: Epoch,
476 pub gl_texture_cache: GlTextureCache,
478 currently_dragging_thumb: Option<ScrollbarDragState>,
480 pub text_input_manager: crate::managers::text_input::TextInputManager,
482 pub undo_redo_manager: crate::managers::undo_redo::UndoRedoManager,
484 pub text_constraints_cache: TextConstraintsCache,
487 pub dirty_text_nodes: BTreeMap<(DomId, NodeId), DirtyTextNode>,
491 pub pending_virtual_view_updates: BTreeMap<DomId, FastBTreeSet<NodeId>>,
494 pub pending_lifecycle_events: Vec<azul_core::events::SyntheticEvent>,
504 pub pending_unmount_invocations: Vec<(
513 azul_core::callbacks::CoreCallbackData,
514 azul_core::events::SyntheticEvent,
515 )>,
516 pub system_style: Option<std::sync::Arc<azul_css::system::SystemStyle>>,
519 pub monitors: std::sync::Arc<std::sync::Mutex<MonitorVec>>,
523 font_stacks_hash: u64,
527 pre_preedit_content: Option<Vec<crate::text3::cache::InlineContent>>,
531 pub input_interpreter: azul_core::events::InputInterpreterCallback,
535 pub post_filter: azul_core::events::PostFilterCallback,
538 pub routes: azul_core::resources::RouteVec,
541 #[cfg(feature = "icu")]
544 pub icu_localizer: IcuLocalizerHandle,
545}
546
547fn default_duration_500ms() -> Duration {
548 Duration::System(SystemTimeDiff::from_millis(500))
549}
550
551fn default_duration_200ms() -> Duration {
552 Duration::System(SystemTimeDiff::from_millis(200))
553}
554
555fn duration_to_millis(duration: Duration) -> u64 {
560 match duration {
561 #[cfg(feature = "std")]
562 Duration::System(system_diff) => {
563 let std_duration: std::time::Duration = system_diff.into();
564 std_duration.as_millis() as u64
565 }
566 #[cfg(not(feature = "std"))]
567 Duration::System(system_diff) => {
568 system_diff.secs * 1000 + (system_diff.nanos / 1_000_000) as u64
570 }
571 Duration::Tick(tick_diff) => {
572 tick_diff.tick_diff
574 }
575 }
576}
577
578impl LayoutWindow {
579 fn from_font_manager(font_manager: FontManager<FontRef>) -> Self {
586 Self {
587 skip_gpu_sync: false,
589 #[cfg(feature = "pdf")]
590 fragmentation_context: crate::paged::FragmentationContext::new_continuous(800.0),
591 layout_cache: Solver3LayoutCache {
592 tree: None,
593 calculated_positions: Vec::new(),
594 viewport: None,
595 scroll_ids: HashMap::new(),
596 scroll_id_to_node_id: HashMap::new(),
597 counters: HashMap::new(),
598 float_cache: HashMap::new(),
599 cache_map: Default::default(),
600 previous_positions: Vec::new(),
601 cached_display_list: None,
602 prev_dom_ptr: 0,
603 prev_viewport: LogicalRect::zero(),
604 },
605 text_cache: TextLayoutCache::new(),
606 font_manager,
607 image_cache: ImageCache::default(),
608 layout_results: BTreeMap::new(),
609 scroll_manager: ScrollManager::new(),
610 gesture_drag_manager: crate::managers::gesture::GestureAndDragManager::new(),
611 focus_manager: crate::managers::focus_cursor::FocusManager::new(),
612 text_edit_manager: crate::managers::text_edit::TextEditManager::new(),
613 file_drop_manager: crate::managers::file_drop::FileDropManager::new(),
614 clipboard_manager: crate::managers::clipboard::ClipboardManager::new(),
615 drag_drop_manager: crate::managers::drag_drop::DragDropManager::new(),
616 hover_manager: crate::managers::hover::HoverManager::new(),
617 virtual_view_manager: VirtualViewManager::new(),
618 gpu_state_manager: GpuStateManager::new(
619 default_duration_500ms(),
620 default_duration_200ms(),
621 ),
622 a11y_manager: crate::managers::a11y::A11yManager::new(),
623 permission_manager: crate::managers::permission::PermissionManager::new(),
624 geolocation_manager: crate::managers::geolocation::GeolocationManager::new(),
625 biometric_manager: crate::managers::biometric::BiometricManager::new(),
626 keyring_manager: crate::managers::keyring::KeyringManager::new(),
627 sensor_manager: crate::managers::sensors::SensorManager::new(),
628 gamepad_manager: crate::managers::gamepad::GamepadManager::new(),
629 safe_area_insets: azul_css::system::SafeAreaInsets::default(),
630 timers: BTreeMap::new(),
631 threads: BTreeMap::new(),
632 renderer_resources: RendererResources::default(),
633 renderer_type: None,
634 previous_window_state: None,
635 current_window_state: FullWindowState::default(),
636 document_id: new_document_id(),
637 id_namespace: new_id_namespace(),
638 epoch: Epoch::new(),
639 gl_texture_cache: GlTextureCache::default(),
640 currently_dragging_thumb: None,
641 text_input_manager: crate::managers::text_input::TextInputManager::new(),
642 undo_redo_manager: crate::managers::undo_redo::UndoRedoManager::new(),
643 text_constraints_cache: TextConstraintsCache {
644 constraints: BTreeMap::new(),
645 },
646 dirty_text_nodes: BTreeMap::new(),
647 pending_virtual_view_updates: BTreeMap::new(),
648 pending_lifecycle_events: Vec::new(),
649 pending_unmount_invocations: Vec::new(),
650 system_style: None,
651 monitors: std::sync::Arc::new(std::sync::Mutex::new(MonitorVec::from_const_slice(&[]))),
652 font_stacks_hash: 0,
653 pre_preedit_content: None,
654 input_interpreter: azul_core::events::InputInterpreterCallback::default(),
655 post_filter: azul_core::events::PostFilterCallback::default(),
656 routes: azul_core::resources::RouteVec::from_const_slice(&[]),
657 #[cfg(feature = "icu")]
658 icu_localizer: IcuLocalizerHandle::default(),
659 }
660 }
661
662 pub fn new(fc_cache: FcFontCache) -> Result<Self, crate::solver3::LayoutError> {
666 Ok(Self::from_font_manager(FontManager::new(fc_cache)?))
667 }
668
669 pub fn from_font_context(ctx: &crate::text3::cache::FontContext) -> Result<Self, crate::solver3::LayoutError> {
673 let fm = ctx.to_font_manager();
674 let fc_cache = fm.fc_cache.clone();
675 let parsed_fonts = fm.parsed_fonts.clone();
676 let mut lw = Self::new_with_shared_fonts(fc_cache, parsed_fonts)?;
677 lw.font_manager = fm;
678 Ok(lw)
679 }
680
681 pub fn new_with_shared_fonts(
683 fc_cache: FcFontCache,
684 parsed_fonts: std::sync::Arc<std::sync::Mutex<std::collections::HashMap<rust_fontconfig::FontId, FontRef>>>,
685 ) -> Result<Self, crate::solver3::LayoutError> {
686 Ok(Self::from_font_manager(FontManager::from_arc_shared(
687 fc_cache,
688 parsed_fonts,
689 )?))
690 }
691
692 #[cfg(feature = "pdf")]
705 pub fn new_paged(
706 fc_cache: FcFontCache,
707 page_size: LogicalSize,
708 ) -> Result<Self, crate::solver3::LayoutError> {
709 let mut lw = Self::from_font_manager(FontManager::new(fc_cache)?);
710 lw.fragmentation_context = crate::paged::FragmentationContext::new_paged(page_size);
711 Ok(lw)
712 }
713
714 pub fn layout_and_generate_display_list(
732 &mut self,
733 root_dom: StyledDom,
734 window_state: &FullWindowState,
735 renderer_resources: &RendererResources,
736 system_callbacks: &ExternalSystemCallbacks,
737 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
738 ) -> Result<(), solver3::LayoutError> {
739 self.layout_results.clear();
741
742 self.virtual_view_manager.reset_all_invocation_flags();
747
748 if let Some(msgs) = debug_messages.as_mut() {
749 msgs.push(LayoutDebugMessage::info(format!(
750 "[layout_and_generate_display_list] Starting layout for DOM with {} nodes",
751 root_dom.node_data.len()
752 )));
753 }
754
755 let result = self.layout_dom_recursive(
758 root_dom,
759 window_state,
760 renderer_resources,
761 system_callbacks,
762 debug_messages,
763 );
764
765 if let Err(ref e) = result {
766 if let Some(msgs) = debug_messages.as_mut() {
767 msgs.push(LayoutDebugMessage::error(format!(
768 "[layout_and_generate_display_list] Layout FAILED: {:?}",
769 e
770 )));
771 }
772 } else {
773 if let Some(msgs) = debug_messages.as_mut() {
774 msgs.push(LayoutDebugMessage::info(format!(
775 "[layout_and_generate_display_list] Layout SUCCESS, layout_results count: {}",
776 self.layout_results.len()
777 )));
778 }
779 }
780
781 #[cfg(feature = "a11y")]
783 if result.is_ok() {
784 self.update_a11y_tree();
785 }
786
787 if result.is_ok() {
789 self.scroll_focused_cursor_into_view();
790 }
791
792 result
793 }
794
795 pub fn layout_dom_recursive(
805 &mut self,
806 styled_dom: StyledDom,
807 window_state: &FullWindowState,
808 renderer_resources: &RendererResources,
809 system_callbacks: &ExternalSystemCallbacks,
810 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
811 ) -> Result<(), solver3::LayoutError> {
812 let dom_id = if styled_dom.dom_id.inner == 0 {
813 DomId::ROOT_ID
814 } else {
815 styled_dom.dom_id
816 };
817
818 let viewport = LogicalRect {
819 origin: LogicalPosition::zero(),
820 size: window_state.size.dimensions,
821 };
822
823 let platform = self.system_style.as_ref()
825 .map(|s| s.platform.clone())
826 .unwrap_or_else(azul_css::system::Platform::current);
827
828 {
831 use crate::{
832 solver3::getters::collect_and_resolve_font_chains_with_registration,
833 text3::default::PathLoader,
834 };
835
836 let compact_cache_ref = styled_dom.css_property_cache.ptr.compact_cache.as_ref();
852 let font_dirty_count = compact_cache_ref
853 .map(|cc| cc.font_dirty_nodes.len())
854 .unwrap_or(1); let font_stacks_sig = compact_cache_ref.map(|cc| {
857 let mut h: u64 = 0xcbf29ce484222325;
863 for &fh in cc.prev_font_hashes.iter() {
864 h = h.rotate_left(13) ^ fh;
865 h = h.wrapping_mul(0x100000001b3);
866 }
867 h
868 });
869
870 let font_requirements_unchanged = (font_dirty_count == 0
875 && !self.font_manager.font_chain_cache.is_empty())
876 || (font_stacks_sig.is_some()
877 && font_stacks_sig == self.font_manager.last_resolved_font_stacks_sig
878 && !self.font_manager.font_chain_cache.is_empty());
879
880 if font_requirements_unchanged {
881 if let Some(msgs) = debug_messages.as_mut() {
882 msgs.push(LayoutDebugMessage::info(
883 "[FontLoading] Font requirements unchanged, skipping resolution (cached)".to_string(),
884 ));
885 }
886 } else {
887 if let Some(msgs) = debug_messages.as_mut() {
888 msgs.push(LayoutDebugMessage::info(
889 "[FontLoading] Starting font resolution for DOM".to_string(),
890 ));
891 }
892
893 if let Some(cc) = styled_dom.css_property_cache.ptr.compact_cache.as_ref() {
896 for (k, v) in cc.font_hash_to_families.iter() {
897 self.font_manager.font_hash_to_families.insert(*k, v.clone());
898 }
899 }
900
901 crate::probe::sample_peak_rss("rss:before_font_chain");
909 let chains = {
910 let _p = crate::probe::Probe::span("font_chain_resolve");
911 collect_and_resolve_font_chains_with_registration(
912 &styled_dom, &self.font_manager.fc_cache, &self.font_manager, &platform,
913 )
914 };
915 crate::probe::sample_peak_rss("rss:after_font_chain");
916
917 if let Some(msgs) = debug_messages.as_mut() {
926 msgs.push(LayoutDebugMessage::info(format!(
927 "[FontLoading] Resolved {} font chains",
928 chains.len()
929 )));
930 }
931
932 let loader = PathLoader::new();
933 crate::probe::sample_peak_rss("rss:before_font_load");
934 let failed = {
935 let _p = crate::probe::Probe::span("font_load_missing");
936 self.font_manager.load_missing_for_chains(
937 &chains,
938 |bytes, index| loader.load_font_shared(bytes, index),
939 )
940 };
941 crate::probe::sample_peak_rss("rss:after_font_load");
942 if let Some(msgs) = debug_messages.as_mut() {
943 for (font_id, error) in &failed {
944 msgs.push(LayoutDebugMessage::warning(format!(
945 "[FontLoading] Failed to load font {:?}: {}",
946 font_id, error
947 )));
948 }
949 }
950
951 self.font_manager.set_font_chain_cache_with_sig(
955 chains.into_fontconfig_chains(),
956 font_stacks_sig,
957 );
958 }
959 }
960
961 let scroll_offsets = self.scroll_manager.get_scroll_states_for_dom(dom_id);
962
963 if !self.skip_gpu_sync {
980 let mut transform_opacity_events = self
981 .gpu_state_manager
982 .get_or_create_cache(dom_id)
983 .synchronize(&styled_dom);
984 self.gpu_state_manager
985 .pending_changes
986 .merge(&mut transform_opacity_events);
987 }
988 let gpu_cache = if self.skip_gpu_sync {
993 GpuValueCache::default()
994 } else {
995 self.gpu_state_manager.get_or_create_cache(dom_id).clone()
996 };
997
998 let cursor_is_visible = self.text_edit_manager.should_draw_cursor();
999 let cursor_locations = self.text_edit_manager.build_cursor_locations();
1000
1001 let mut display_list = {
1002 let _p = crate::probe::Probe::span("solver3_layout_document");
1003 solver3::layout_document(
1004 &mut self.layout_cache,
1005 &mut self.text_cache,
1006 &styled_dom,
1007 viewport,
1008 &self.font_manager,
1009 &scroll_offsets,
1010 &std::collections::BTreeMap::new(),
1011 debug_messages,
1012 Some(&gpu_cache),
1013 &self.renderer_resources,
1014 self.id_namespace,
1015 dom_id,
1016 cursor_is_visible,
1017 cursor_locations,
1018 self.text_edit_manager.preedit_text.clone(),
1019 &self.image_cache,
1020 self.system_style.clone(),
1021 system_callbacks.get_system_time_fn,
1022 )?
1023 };
1024
1025 crate::probe::hint_purge_allocator();
1028
1029 if self.skip_gpu_sync {
1040 if let Some(tree) = self.layout_cache.tree.clone() {
1041 self.layout_results.insert(
1042 dom_id,
1043 DomLayoutResult {
1044 styled_dom,
1045 layout_tree: tree,
1046 calculated_positions: self.layout_cache.calculated_positions.clone(),
1047 viewport,
1048 display_list: DisplayList::default(),
1049 scroll_ids: self.layout_cache.scroll_ids.clone(),
1050 scroll_id_to_node_id: self.layout_cache.scroll_id_to_node_id.clone(),
1051 },
1052 );
1053 }
1054 return Ok(());
1055 }
1056
1057 static MEM_BREAKDOWN_ENABLED: std::sync::OnceLock<bool> =
1061 std::sync::OnceLock::new();
1062 if *MEM_BREAKDOWN_ENABLED.get_or_init(azul_core::profile::memory_enabled) {
1063 let sr = styled_dom.memory_report();
1064 eprintln!("[MEM] StyledDom ({} nodes) total={} KiB", sr.node_count, sr.total_bytes() / 1024);
1065 eprintln!("[MEM] node_hierarchy {:>7} KiB", sr.node_hierarchy_bytes / 1024);
1066 eprintln!("[MEM] node_data {:>7} KiB", sr.node_data_bytes / 1024);
1067 eprintln!("[MEM] styled_nodes {:>7} KiB", sr.styled_nodes_bytes / 1024);
1068 eprintln!("[MEM] cascade_info {:>7} KiB", sr.cascade_info_bytes / 1024);
1069 eprintln!("[MEM] tag_ids {:>7} KiB", sr.tag_ids_bytes / 1024);
1070 eprintln!("[MEM] non_leaf_nodes {:>7} KiB", sr.non_leaf_nodes_bytes / 1024);
1071 let bd = &sr.css_property_cache;
1072 eprintln!("[MEM] CssPropertyCache {:>7} KiB", bd.total_bytes() / 1024);
1073 eprintln!("[MEM] cascaded_props {:>6} KiB", bd.cascaded_props_bytes / 1024);
1074 eprintln!("[MEM] css_props {:>6} KiB", bd.css_props_bytes / 1024);
1075 eprintln!("[MEM] computed_values {:>7} KiB", bd.computed_values_bytes / 1024);
1076 eprintln!("[MEM] user_overridden {:>7} KiB", bd.user_overridden_bytes / 1024);
1077 eprintln!("[MEM] global_css_props {:>7} KiB", bd.global_css_props_bytes / 1024);
1078 eprintln!("[MEM] compact_cache {:>7} KiB", bd.compact_cache_bytes / 1024);
1079 eprintln!("[MEM] resolved_font_sz {:>7} KiB", bd.resolved_font_sizes_bytes / 1024);
1080
1081 let sc = self.layout_cache.memory_report();
1083 eprintln!("[MEM] Solver3 LayoutCache total={} KiB", sc.total_bytes() / 1024);
1084 if let Some(tr) = &sc.tree_report {
1085 eprintln!("[MEM] LayoutTree {:>7} KiB ({} nodes)", sc.tree_bytes / 1024, tr.node_count);
1086 eprintln!("[MEM] hot {:>6} KiB", tr.hot_bytes / 1024);
1087 eprintln!("[MEM] warm {:>6} KiB", tr.warm_bytes / 1024);
1088 eprintln!("[MEM] warm.inline {:>6} KiB (shaped text in CachedInlineLayout)", tr.warm_inline_layout_bytes / 1024);
1089 eprintln!("[MEM] warm.taffy {:>6} KiB", tr.warm_taffy_cache_bytes / 1024);
1090 eprintln!("[MEM] cold {:>6} KiB", tr.cold_bytes / 1024);
1091 eprintln!("[MEM] children_arena {:>6} KiB", tr.children_arena_bytes / 1024);
1092 eprintln!("[MEM] dom_to_layout {:>6} KiB", tr.dom_to_layout_bytes / 1024);
1093 }
1094 eprintln!("[MEM] cache_map {:>7} KiB (Taffy-style 9+1 slots per node)", sc.cache_map_bytes / 1024);
1095 eprintln!("[MEM] calculated_pos {:>7} KiB", sc.calculated_positions_bytes / 1024);
1096 eprintln!("[MEM] previous_pos {:>7} KiB", sc.previous_positions_bytes / 1024);
1097 eprintln!("[MEM] float_cache {:>7} KiB", sc.float_cache_bytes / 1024);
1098 eprintln!("[MEM] counters {:>7} KiB", sc.counters_bytes / 1024);
1099 eprintln!("[MEM] scroll_ids {:>7} KiB", sc.scroll_ids_bytes / 1024);
1100 eprintln!("[MEM] cached_display {:>7} KiB", sc.cached_display_list_bytes / 1024);
1101
1102 let tc = self.text_cache.memory_report();
1104 eprintln!("[MEM] TextShapingCache total={} KiB", tc.total_bytes() / 1024);
1105 eprintln!("[MEM] logical_items {:>7} KiB ({} entries)", tc.logical_items_bytes / 1024, tc.logical_items_entries);
1106 eprintln!("[MEM] visual_items {:>7} KiB ({} entries)", tc.visual_items_bytes / 1024, tc.visual_items_entries);
1107 eprintln!("[MEM] shaped_items {:>7} KiB ({} entries)", tc.shaped_items_bytes / 1024, tc.shaped_items_entries);
1108 eprintln!("[MEM] glyph_bytes {:>7} KiB", tc.shaped_glyph_bytes / 1024);
1109 eprintln!("[MEM] cluster_text {:>7} KiB", tc.shaped_cluster_text_bytes / 1024);
1110 eprintln!("[MEM] per_item_shaped {:>7} KiB ({} entries)", tc.per_item_shaped_bytes / 1024, tc.per_item_shaped_entries);
1111
1112 let grand_total = sr.total_bytes() + sc.total_bytes() + tc.total_bytes();
1113 eprintln!("[MEM] --- GRAND TOTAL (StyledDom + Solver3 + TextCache) = {} KiB = {:.2} MiB ---",
1114 grand_total / 1024, grand_total as f64 / 1048576.0);
1115
1116 #[cfg(feature = "probe")]
1117 {
1118 let (rss, _virt) = crate::probe::current_rss_bytes();
1119 let peak = crate::probe::peak_rss_bytes_pub();
1120 eprintln!("[MEM] after layout: current rss={:.1} MiB peak rss={:.1} MiB (unreturned={:.1} MiB)",
1121 rss as f64 / 1048576.0, peak as f64 / 1048576.0,
1122 (peak.saturating_sub(rss)) as f64 / 1048576.0);
1123 eprintln!("[MEM] accounted / rss = {:.1}% — the gap is allocator overhead + unreturned transient pages + fonts/images + misc",
1124 grand_total as f64 * 100.0 / (rss as f64).max(1.0));
1125 }
1126 }
1127
1128 static CPU_ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1134 if *CPU_ENABLED.get_or_init(azul_core::profile::cpu_enabled) {
1135 let events = crate::probe::Probe::drain();
1136 crate::probe::print_drained_events("layout pass", &events);
1137 }
1138
1139 static CASCADE_ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1143 if *CASCADE_ENABLED.get_or_init(azul_core::profile::cascade_enabled) {
1144 let counts = azul_core::prop_cache::drain_css_prop_counts();
1145 let total: usize = counts.iter().map(|(_, n)| *n).sum();
1146 if total > 0 {
1147 eprintln!("[CASCADE] cascade-walks this pass: {} total", total);
1148 for (label, n) in counts.iter().take(20) {
1149 eprintln!("[CASCADE] {:>8} {}", n, label);
1150 }
1151 }
1152 }
1153
1154 let tree = self
1155 .layout_cache
1156 .tree
1157 .clone()
1158 .ok_or(solver3::LayoutError::InvalidTree)?;
1159
1160 let scroll_ids = self.layout_cache.scroll_ids.clone();
1162 let scroll_id_to_node_id = self.layout_cache.scroll_id_to_node_id.clone();
1163
1164 {
1170 use crate::solver3::display_list::{DisplayListItem, ScrollbarDrawInfo};
1171 let gpu_cache = self.gpu_state_manager.get_or_create_cache(dom_id);
1172 for item in &display_list.items {
1173 if let DisplayListItem::ScrollBarStyled { info } = item {
1174 if let Some(hit_id) = &info.hit_id {
1175 if let Some(transform_key) = info.thumb_transform_key {
1177 match hit_id {
1178 azul_core::hit_test::ScrollbarHitId::VerticalThumb(_, nid) => {
1179 if !gpu_cache.transform_keys.contains_key(nid) {
1180 gpu_cache.transform_keys.insert(*nid, transform_key);
1181 gpu_cache.current_transform_values.insert(*nid, info.thumb_initial_transform);
1182 }
1183 }
1184 azul_core::hit_test::ScrollbarHitId::HorizontalThumb(_, nid) => {
1185 if !gpu_cache.h_transform_keys.contains_key(nid) {
1186 gpu_cache.h_transform_keys.insert(*nid, transform_key);
1187 gpu_cache.h_current_transform_values.insert(*nid, info.thumb_initial_transform);
1188 }
1189 }
1190 _ => {}
1191 }
1192 }
1193
1194 let initial_opacity = if info.visibility == azul_css::props::style::scrollbar::ScrollbarVisibilityMode::Always {
1205 1.0
1206 } else {
1207 0.0
1208 };
1209 if let Some(opacity_key) = info.opacity_key {
1210 match hit_id {
1211 azul_core::hit_test::ScrollbarHitId::VerticalThumb(_, nid) => {
1212 let key = (dom_id, *nid);
1213 if !gpu_cache.scrollbar_v_opacity_keys.contains_key(&key) {
1214 gpu_cache.scrollbar_v_opacity_keys.insert(key, opacity_key);
1215 gpu_cache.scrollbar_v_opacity_values.insert(key, initial_opacity);
1216 }
1217 }
1218 azul_core::hit_test::ScrollbarHitId::HorizontalThumb(_, nid) => {
1219 let key = (dom_id, *nid);
1220 if !gpu_cache.scrollbar_h_opacity_keys.contains_key(&key) {
1221 gpu_cache.scrollbar_h_opacity_keys.insert(key, opacity_key);
1222 gpu_cache.scrollbar_h_opacity_values.insert(key, initial_opacity);
1223 }
1224 }
1225 _ => {}
1226 }
1227 }
1228 }
1229 }
1230 }
1231 }
1232
1233 self.gpu_state_manager
1235 .update_scrollbar_transforms(dom_id, &self.scroll_manager, &tree);
1236
1237 let vviews = self.scan_for_virtual_views(&styled_dom, &tree, &self.layout_cache.calculated_positions);
1240
1241 for (node_id, bounds) in vviews {
1242 if let Some(child_dom_id) = self.invoke_virtual_view_callback_with_dom(
1243 dom_id,
1244 node_id,
1245 bounds,
1246 Some(&styled_dom),
1247 window_state,
1248 renderer_resources,
1249 system_callbacks,
1250 debug_messages,
1251 ) {
1252 let mut replaced = false;
1256 for item in display_list.items.iter_mut() {
1257 if let crate::solver3::display_list::DisplayListItem::VirtualViewPlaceholder {
1258 node_id: ref placeholder_nid,
1259 bounds: ref placeholder_bounds,
1260 clip_rect: ref placeholder_clip,
1261 ..
1262 } = item
1263 {
1264 if *placeholder_nid == node_id {
1265 *item = crate::solver3::display_list::DisplayListItem::VirtualView {
1266 child_dom_id,
1267 bounds: *placeholder_bounds,
1268 clip_rect: *placeholder_clip,
1269 };
1270 replaced = true;
1271 break;
1272 }
1273 }
1274 }
1275
1276 if !replaced {
1277 display_list
1279 .items
1280 .push(crate::solver3::display_list::DisplayListItem::VirtualView {
1281 child_dom_id,
1282 bounds: bounds.into(),
1283 clip_rect: bounds.into(),
1284 });
1285 }
1286 }
1287 }
1288
1289 self.layout_results.insert(
1292 dom_id,
1293 DomLayoutResult {
1294 styled_dom,
1295 layout_tree: tree,
1296 calculated_positions: self.layout_cache.calculated_positions.clone(),
1297 viewport,
1298 display_list,
1299 scroll_ids,
1300 scroll_id_to_node_id,
1301 },
1302 );
1303
1304 self.scroll_manager.clear_scroll_dirty();
1307
1308 Ok(())
1309 }
1310
1311 fn scan_for_virtual_views(
1312 &self,
1313 styled_dom: &StyledDom,
1314 layout_tree: &LayoutTree,
1315 calculated_positions: &crate::solver3::PositionVec,
1316 ) -> Vec<(NodeId, LogicalRect)> {
1317 let node_data_container = styled_dom.node_data.as_container();
1318 layout_tree
1319 .nodes
1320 .iter()
1321 .enumerate()
1322 .filter_map(|(idx, node)| {
1323 let node_dom_id = node.dom_node_id?;
1324 let node_data = node_data_container.get(node_dom_id)?;
1325 if matches!(node_data.get_node_type(), NodeType::VirtualView) {
1326 let pos = calculated_positions.get(idx).copied().unwrap_or_default();
1327 let size = node.used_size.unwrap_or_default();
1328 Some((node_dom_id, LogicalRect::new(pos, size)))
1329 } else {
1330 None
1331 }
1332 })
1333 .collect()
1334 }
1335
1336 pub fn resize_window(
1343 &mut self,
1344 styled_dom: StyledDom,
1345 new_size: LogicalSize,
1346 renderer_resources: &RendererResources,
1347 system_callbacks: &ExternalSystemCallbacks,
1348 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1349 ) -> Result<DisplayList, crate::solver3::LayoutError> {
1350 let mut window_state = FullWindowState::default();
1352 window_state.size.dimensions = new_size;
1353
1354 let dom_id = styled_dom.dom_id;
1355
1356 self.layout_and_generate_display_list(
1357 styled_dom,
1358 &window_state,
1359 renderer_resources,
1360 system_callbacks,
1361 debug_messages,
1362 )?;
1363
1364 self.layout_results
1367 .get_mut(&dom_id)
1368 .map(|result| std::mem::replace(&mut result.display_list, DisplayList::default()))
1369 .ok_or(solver3::LayoutError::InvalidTree)
1370 }
1371
1372 pub fn clear_caches(&mut self) {
1374 self.layout_cache = Solver3LayoutCache {
1375 tree: None,
1376 calculated_positions: Vec::new(),
1377 viewport: None,
1378 scroll_ids: HashMap::new(),
1379 scroll_id_to_node_id: HashMap::new(),
1380 counters: HashMap::new(),
1381 float_cache: HashMap::new(),
1382 cache_map: Default::default(),
1383 previous_positions: Vec::new(),
1384 cached_display_list: None,
1385 prev_dom_ptr: 0,
1386 prev_viewport: LogicalRect::zero(),
1387 };
1388 self.text_cache = TextLayoutCache::new();
1389 self.layout_results.clear();
1390 self.scroll_manager = ScrollManager::new();
1391 }
1392
1393 pub fn set_scroll_position(&mut self, dom_id: DomId, node_id: NodeId, scroll: ScrollPosition) {
1395 #[cfg(feature = "std")]
1397 let now = Instant::System(std::time::Instant::now().into());
1398 #[cfg(not(feature = "std"))]
1399 let now = Instant::Tick(azul_core::task::SystemTick { tick_counter: 0 });
1400
1401 self.scroll_manager.update_node_bounds(
1402 dom_id,
1403 node_id,
1404 scroll.parent_rect,
1405 scroll.children_rect,
1406 now.clone(),
1407 );
1408 self.scroll_manager
1409 .set_scroll_position(dom_id, node_id, scroll.children_rect.origin, now);
1410 }
1411
1412 pub fn get_scroll_position(&self, dom_id: DomId, node_id: NodeId) -> Option<ScrollPosition> {
1414 let states = self.scroll_manager.get_scroll_states_for_dom(dom_id);
1415 states.get(&node_id).cloned()
1416 }
1417
1418 pub fn set_selection(&mut self, _dom_id: DomId, _selection: SelectionState) {
1420 }
1422
1423 pub fn get_selection(&self, _dom_id: DomId) -> Option<&SelectionState> {
1425 None
1426 }
1427
1428 pub fn invoke_virtual_view_callback(
1438 &mut self,
1439 parent_dom_id: DomId,
1440 node_id: NodeId,
1441 bounds: LogicalRect,
1442 window_state: &FullWindowState,
1443 renderer_resources: &RendererResources,
1444 system_callbacks: &ExternalSystemCallbacks,
1445 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1446 ) -> Option<DomId> {
1447 self.invoke_virtual_view_callback_with_dom(
1448 parent_dom_id, node_id, bounds, None,
1449 window_state, renderer_resources, system_callbacks, debug_messages,
1450 )
1451 }
1452
1453 fn invoke_virtual_view_callback_with_dom(
1457 &mut self,
1458 parent_dom_id: DomId,
1459 node_id: NodeId,
1460 bounds: LogicalRect,
1461 styled_dom_override: Option<&StyledDom>,
1462 window_state: &FullWindowState,
1463 renderer_resources: &RendererResources,
1464 system_callbacks: &ExternalSystemCallbacks,
1465 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1466 ) -> Option<DomId> {
1467 if let Some(msgs) = debug_messages {
1468 msgs.push(LayoutDebugMessage::info(format!(
1469 "invoke_virtual_view_callback called for node {:?}",
1470 node_id
1471 )));
1472 }
1473
1474 let virtual_view_node = if let Some(styled_dom) = styled_dom_override {
1476 let node_data_container = styled_dom.node_data.as_container();
1477 let node_data = node_data_container.get(node_id)?;
1478 node_data.get_virtual_view_node_ref()?.clone()
1479 } else {
1480 let layout_result = self.layout_results.get(&parent_dom_id)?;
1481 if let Some(msgs) = debug_messages {
1482 msgs.push(LayoutDebugMessage::info(format!(
1483 "Got layout result for parent DOM {:?}",
1484 parent_dom_id
1485 )));
1486 }
1487 let node_data_container = layout_result.styled_dom.node_data.as_container();
1488 let node_data = node_data_container.get(node_id)?;
1489 match node_data.get_virtual_view_node_ref() {
1490 Some(vv) => vv.clone(),
1491 None => {
1492 if let Some(msgs) = debug_messages {
1493 msgs.push(LayoutDebugMessage::info(format!(
1494 "Node is NOT VirtualView, type = {:?}",
1495 node_data.get_node_type()
1496 )));
1497 }
1498 return None;
1499 }
1500 }
1501 };
1502
1503 if let Some(msgs) = debug_messages {
1504 msgs.push(LayoutDebugMessage::info("Node is VirtualView type".to_string()));
1505 }
1506
1507 self.invoke_virtual_view_callback_impl(
1509 parent_dom_id,
1510 node_id,
1511 &virtual_view_node,
1512 bounds,
1513 window_state,
1514 renderer_resources,
1515 system_callbacks,
1516 debug_messages,
1517 )
1518 }
1519
1520 fn invoke_virtual_view_callback_impl(
1531 &mut self,
1532 parent_dom_id: DomId,
1533 node_id: NodeId,
1534 virtual_view_node: &azul_core::dom::VirtualViewNode,
1535 bounds: LogicalRect,
1536 window_state: &FullWindowState,
1537 renderer_resources: &RendererResources,
1538 system_callbacks: &ExternalSystemCallbacks,
1539 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1540 ) -> Option<DomId> {
1541 let now = (system_callbacks.get_system_time_fn.cb)();
1543
1544 self.scroll_manager.update_node_bounds(
1547 parent_dom_id,
1548 node_id,
1549 bounds,
1550 LogicalRect::new(LogicalPosition::zero(), bounds.size), now.clone(),
1552 );
1553
1554 let reason = match self.virtual_view_manager.check_reinvoke(
1557 parent_dom_id,
1558 node_id,
1559 &self.scroll_manager,
1560 bounds,
1561 ) {
1562 Some(r) => r,
1563 None => {
1564 return self
1566 .virtual_view_manager
1567 .get_nested_dom_id(parent_dom_id, node_id);
1568 }
1569 };
1570
1571 if let Some(msgs) = debug_messages {
1572 msgs.push(LayoutDebugMessage::info(format!(
1573 "VirtualView ({:?}, {:?}) - Reason: {:?}",
1574 parent_dom_id, node_id, reason
1575 )));
1576 }
1577
1578 let scroll_offset = self
1579 .scroll_manager
1580 .get_current_offset(parent_dom_id, node_id)
1581 .unwrap_or_default();
1582
1583 let hidpi_factor = window_state.size.get_hidpi_factor();
1584
1585 let mut callback_info = azul_core::callbacks::VirtualViewCallbackInfo::new(
1587 reason,
1588 &self.font_manager.fc_cache,
1589 &self.image_cache,
1590 window_state.theme,
1591 azul_core::callbacks::HidpiAdjustedBounds {
1592 logical_size: bounds.size,
1593 hidpi_factor,
1594 },
1595 bounds.size,
1596 scroll_offset,
1597 bounds.size,
1598 LogicalPosition::zero(),
1599 );
1600
1601 let callback_data = virtual_view_node.refany.clone();
1603
1604 let callback_return = (virtual_view_node.callback.cb)(callback_data, callback_info);
1606
1607 self.virtual_view_manager
1609 .mark_invoked(parent_dom_id, node_id, reason);
1610
1611 let mut child_styled_dom = match callback_return.dom {
1613 azul_core::dom::OptionDom::Some(dom) => {
1614 azul_core::styled_dom::StyledDom::create_from_dom(dom)
1616 },
1617 azul_core::dom::OptionDom::None => {
1618 if reason == VirtualViewCallbackReason::InitialRender {
1620 let mut empty_dom = Dom::create_div();
1622 let empty_css = Css::empty();
1623 azul_core::styled_dom::StyledDom::create(&mut empty_dom, empty_css)
1624 } else {
1625 self.virtual_view_manager.update_virtual_view_info(
1628 parent_dom_id,
1629 node_id,
1630 callback_return.scroll_size,
1631 callback_return.virtual_scroll_size,
1632 );
1633 self.scroll_manager.update_virtual_scroll_bounds(
1635 parent_dom_id,
1636 node_id,
1637 callback_return.virtual_scroll_size,
1638 Some(callback_return.scroll_offset),
1639 );
1640 return self
1641 .virtual_view_manager
1642 .get_nested_dom_id(parent_dom_id, node_id);
1643 }
1644 }
1645 };
1646
1647 let child_dom_id = self
1649 .virtual_view_manager
1650 .get_or_create_nested_dom_id(parent_dom_id, node_id);
1651 child_styled_dom.dom_id = child_dom_id;
1652
1653 self.virtual_view_manager.update_virtual_view_info(
1655 parent_dom_id,
1656 node_id,
1657 callback_return.scroll_size,
1658 callback_return.virtual_scroll_size,
1659 );
1660 self.scroll_manager.update_virtual_scroll_bounds(
1662 parent_dom_id,
1663 node_id,
1664 callback_return.virtual_scroll_size,
1665 Some(callback_return.scroll_offset),
1666 );
1667
1668 self.layout_dom_recursive(
1673 child_styled_dom,
1674 window_state,
1675 renderer_resources,
1676 system_callbacks,
1677 debug_messages,
1678 )
1679 .ok()?;
1680
1681 Some(child_dom_id)
1682 }
1683
1684 pub fn get_node_size(&self, node_id: DomNodeId) -> Option<LogicalSize> {
1688 unsafe { core::ptr::write_volatile(0x400EC as *mut u32, 0xE6_000001u32 | ((self.layout_results.len() as u32 & 0xff) << 8)); }
1690 let layout_result = match self.layout_results.get(&node_id.dom) {
1691 Some(r) => r,
1692 None => { unsafe { core::ptr::write_volatile(0x400EC as *mut u32, 0xE6_0000FAu32); } return None; }
1693 };
1694 let nid = node_id.node.into_crate_internal()?;
1695 unsafe { core::ptr::write_volatile(0x400EC as *mut u32, 0xE6_000002u32 | ((layout_result.layout_tree.dom_to_layout.len() as u32 & 0xfff) << 8)); }
1696 let layout_indices = match layout_result.layout_tree.dom_to_layout.get(&nid) {
1698 Some(x) => x,
1699 None => { unsafe { core::ptr::write_volatile(0x400EC as *mut u32, 0xE6_0000FBu32); } return None; }
1700 };
1701 let layout_index = *layout_indices.first()?;
1702 let layout_node = match layout_result.layout_tree.get(layout_index) {
1703 Some(n) => n,
1704 None => { unsafe { core::ptr::write_volatile(0x400EC as *mut u32, 0xE6_0000FCu32); } return None; }
1705 };
1706 match layout_node.used_size {
1707 Some(s) => { unsafe { core::ptr::write_volatile(0x400EC as *mut u32, 0xE6_000004u32 | (((s.width as u32) & 0xffff) << 8)); } Some(s) }
1708 None => { unsafe { core::ptr::write_volatile(0x400EC as *mut u32, 0xE6_0000FDu32); } None }
1709 }
1710 }
1711
1712 pub fn get_node_position(&self, node_id: DomNodeId) -> Option<LogicalPosition> {
1714 let layout_result = self.layout_results.get(&node_id.dom)?;
1715 let nid = node_id.node.into_crate_internal()?;
1716 let layout_indices = layout_result.layout_tree.dom_to_layout.get(&nid)?;
1718 let layout_index = *layout_indices.first()?;
1719 let position = layout_result.calculated_positions.get(layout_index)?;
1720 Some(*position)
1721 }
1722
1723 pub fn get_node_hit_test_bounds(&self, node_id: DomNodeId) -> Option<LogicalRect> {
1729 use crate::solver3::display_list::DisplayListItem;
1730
1731 let layout_result = self.layout_results.get(&node_id.dom)?;
1732 let nid = node_id.node.into_crate_internal()?;
1733
1734 let nid_encoded = NodeHierarchyItemId::from_crate_internal(Some(nid));
1736 let tag_id = layout_result.styled_dom.tag_ids_to_node_ids.iter()
1737 .find(|m| m.node_id == nid_encoded)?
1738 .tag_id
1739 .inner;
1740
1741 for item in &layout_result.display_list.items {
1744 if let DisplayListItem::HitTestArea { bounds, tag } = item {
1745 if tag.0 == tag_id && bounds.0.size.width > 0.0 && bounds.0.size.height > 0.0 {
1746 return Some(bounds.0);
1747 }
1748 }
1749 }
1750 None
1751 }
1752
1753 pub fn get_parent(&self, node_id: DomNodeId) -> Option<DomNodeId> {
1755 let layout_result = self.layout_results.get(&node_id.dom)?;
1756 let nid = node_id.node.into_crate_internal()?;
1757 let parent_id = layout_result
1758 .styled_dom
1759 .node_hierarchy
1760 .as_container()
1761 .get(nid)?
1762 .parent_id()?;
1763 Some(DomNodeId {
1764 dom: node_id.dom,
1765 node: NodeHierarchyItemId::from_crate_internal(Some(parent_id)),
1766 })
1767 }
1768
1769 pub fn get_first_child(&self, node_id: DomNodeId) -> Option<DomNodeId> {
1771 let layout_result = self.layout_results.get(&node_id.dom)?;
1772 let nid = node_id.node.into_crate_internal()?;
1773 let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
1774 let hierarchy_item = node_hierarchy.get(nid)?;
1775 let first_child_id = hierarchy_item.first_child_id(nid)?;
1776 Some(DomNodeId {
1777 dom: node_id.dom,
1778 node: NodeHierarchyItemId::from_crate_internal(Some(first_child_id)),
1779 })
1780 }
1781
1782 pub fn get_next_sibling(&self, node_id: DomNodeId) -> Option<DomNodeId> {
1784 let layout_result = self.layout_results.get(&node_id.dom)?;
1785 let nid = node_id.node.into_crate_internal()?;
1786 let next_sibling_id = layout_result
1787 .styled_dom
1788 .node_hierarchy
1789 .as_container()
1790 .get(nid)?
1791 .next_sibling_id()?;
1792 Some(DomNodeId {
1793 dom: node_id.dom,
1794 node: NodeHierarchyItemId::from_crate_internal(Some(next_sibling_id)),
1795 })
1796 }
1797
1798 pub fn get_previous_sibling(&self, node_id: DomNodeId) -> Option<DomNodeId> {
1800 let layout_result = self.layout_results.get(&node_id.dom)?;
1801 let nid = node_id.node.into_crate_internal()?;
1802 let prev_sibling_id = layout_result
1803 .styled_dom
1804 .node_hierarchy
1805 .as_container()
1806 .get(nid)?
1807 .previous_sibling_id()?;
1808 Some(DomNodeId {
1809 dom: node_id.dom,
1810 node: NodeHierarchyItemId::from_crate_internal(Some(prev_sibling_id)),
1811 })
1812 }
1813
1814 pub fn get_last_child(&self, node_id: DomNodeId) -> Option<DomNodeId> {
1816 let layout_result = self.layout_results.get(&node_id.dom)?;
1817 let nid = node_id.node.into_crate_internal()?;
1818 let last_child_id = layout_result
1819 .styled_dom
1820 .node_hierarchy
1821 .as_container()
1822 .get(nid)?
1823 .last_child_id()?;
1824 Some(DomNodeId {
1825 dom: node_id.dom,
1826 node: NodeHierarchyItemId::from_crate_internal(Some(last_child_id)),
1827 })
1828 }
1829
1830 pub fn scan_used_fonts(&self) -> BTreeSet<FontKey> {
1837 use crate::solver3::display_list::DisplayListItem;
1838
1839 let mut fonts = BTreeSet::new();
1840 for (_dom_id, layout_result) in &self.layout_results {
1841 for item in &layout_result.display_list.items {
1842 let hash = match item {
1843 DisplayListItem::Text { font_hash, .. } => font_hash.font_hash,
1844 DisplayListItem::TextLayout { font_hash, .. } => font_hash.font_hash,
1845 _ => continue,
1846 };
1847 let ns = (hash >> 32) as u32;
1849 let ns = if ns == 0 { 1 } else { ns };
1850 fonts.insert(FontKey {
1851 namespace: IdNamespace(ns),
1852 key: hash,
1853 });
1854 }
1855 }
1856 fonts
1857 }
1858
1859 pub fn scan_used_images(&self, _css_image_cache: &ImageCache) -> BTreeSet<ImageRefHash> {
1865 use crate::solver3::display_list::DisplayListItem;
1866
1867 let mut images = BTreeSet::new();
1868 for (_dom_id, layout_result) in &self.layout_results {
1869 for item in &layout_result.display_list.items {
1870 match item {
1871 DisplayListItem::Image { image, .. } => {
1872 images.insert(image.get_hash());
1873 }
1874 DisplayListItem::PushImageMaskClip { mask_image, .. } => {
1875 images.insert(mask_image.get_hash());
1876 }
1877 _ => {}
1878 }
1879 }
1880 }
1881 images
1882 }
1883
1884 fn get_nested_scroll_states(
1886 &self,
1887 dom_id: DomId,
1888 ) -> BTreeMap<DomId, BTreeMap<NodeHierarchyItemId, ScrollPosition>> {
1889 let mut nested = BTreeMap::new();
1890 let scroll_states = self.scroll_manager.get_scroll_states_for_dom(dom_id);
1891 let mut inner = BTreeMap::new();
1892 for (node_id, scroll_pos) in scroll_states {
1893 inner.insert(
1894 NodeHierarchyItemId::from_crate_internal(Some(node_id)),
1895 scroll_pos,
1896 );
1897 }
1898 nested.insert(dom_id, inner);
1899 nested
1900 }
1901
1902 pub fn scroll_node_into_view(
1921 &mut self,
1922 node_id: DomNodeId,
1923 options: crate::managers::scroll_into_view::ScrollIntoViewOptions,
1924 now: azul_core::task::Instant,
1925 ) -> Vec<crate::managers::scroll_into_view::ScrollAdjustment> {
1926 crate::managers::scroll_into_view::scroll_node_into_view(
1927 node_id,
1928 &self.layout_results,
1929 &mut self.scroll_manager,
1930 options,
1931 now,
1932 )
1933 }
1934
1935 pub fn scroll_cursor_into_view(
1940 &mut self,
1941 cursor_rect: LogicalRect,
1942 node_id: DomNodeId,
1943 options: crate::managers::scroll_into_view::ScrollIntoViewOptions,
1944 now: azul_core::task::Instant,
1945 ) -> Vec<crate::managers::scroll_into_view::ScrollAdjustment> {
1946 crate::managers::scroll_into_view::scroll_cursor_into_view(
1947 cursor_rect,
1948 node_id,
1949 &self.layout_results,
1950 &mut self.scroll_manager,
1951 options,
1952 now,
1953 )
1954 }
1955
1956 pub fn add_timer(&mut self, timer_id: TimerId, timer: Timer) {
1960 self.timers.insert(timer_id, timer);
1961 }
1962
1963 pub fn remove_timer(&mut self, timer_id: &TimerId) -> Option<Timer> {
1965 self.timers.remove(timer_id)
1966 }
1967
1968 pub fn get_timer(&self, timer_id: &TimerId) -> Option<&Timer> {
1970 self.timers.get(timer_id)
1971 }
1972
1973 pub fn get_timer_mut(&mut self, timer_id: &TimerId) -> Option<&mut Timer> {
1975 self.timers.get_mut(timer_id)
1976 }
1977
1978 pub fn get_timer_ids(&self) -> TimerIdVec {
1980 self.timers.keys().copied().collect::<Vec<_>>().into()
1981 }
1982
1983 pub fn tick_timers(&mut self, current_time: azul_core::task::Instant) -> Vec<TimerId> {
1986 let mut ready_timers = Vec::new();
1987
1988 for (timer_id, timer) in &mut self.timers {
1989 ready_timers.push(*timer_id);
1994 }
1995
1996 ready_timers
1997 }
1998
1999 pub fn time_until_next_timer_ms(
2008 &self,
2009 get_system_time_fn: &azul_core::task::GetSystemTimeCallback,
2010 ) -> Option<u64> {
2011 if self.timers.is_empty() {
2012 return None; }
2014
2015 let now = (get_system_time_fn.cb)();
2016 let mut min_ms: Option<u64> = None;
2017
2018 for timer in self.timers.values() {
2019 let next_run = timer.instant_of_next_run();
2020
2021 let ms_until = if next_run < now {
2023 0 } else {
2025 duration_to_millis(next_run.duration_since(&now))
2026 };
2027
2028 min_ms = Some(match min_ms {
2029 Some(current_min) => current_min.min(ms_until),
2030 None => ms_until,
2031 });
2032 }
2033
2034 min_ms
2035 }
2036
2037 pub fn add_thread(&mut self, thread_id: ThreadId, thread: Thread) {
2041 self.threads.insert(thread_id, thread);
2042 }
2043
2044 pub fn remove_thread(&mut self, thread_id: &ThreadId) -> Option<Thread> {
2046 self.threads.remove(thread_id)
2047 }
2048
2049 pub fn get_thread(&self, thread_id: &ThreadId) -> Option<&Thread> {
2051 self.threads.get(thread_id)
2052 }
2053
2054 pub fn get_thread_mut(&mut self, thread_id: &ThreadId) -> Option<&mut Thread> {
2056 self.threads.get_mut(thread_id)
2057 }
2058
2059 pub fn get_thread_ids(&self) -> ThreadIdVec {
2061 self.threads.keys().copied().collect::<Vec<_>>().into()
2062 }
2063
2064 pub fn create_cursor_blink_timer(&self, _window_state: &FullWindowState) -> crate::timer::Timer {
2072 use azul_core::task::{Duration, SystemTimeDiff};
2073 use crate::timer::{Timer, TimerCallback};
2074 use azul_core::refany::RefAny;
2075
2076 let interval_ms = crate::managers::text_edit::CURSOR_BLINK_INTERVAL_MS;
2077
2078 let refany = RefAny::new(());
2081
2082 Timer {
2083 refany,
2084 node_id: None.into(),
2085 created: azul_core::task::Instant::now(),
2086 run_count: 0,
2087 last_run: azul_core::task::OptionInstant::None,
2088 delay: azul_core::task::OptionDuration::None,
2089 interval: azul_core::task::OptionDuration::Some(Duration::System(SystemTimeDiff::from_millis(interval_ms))),
2090 timeout: azul_core::task::OptionDuration::None,
2091 callback: TimerCallback::create(cursor_blink_timer_callback),
2092 }
2093 }
2094
2095 pub fn create_tooltip_delay_timer(&self, hover_time_ms: u32) -> crate::timer::Timer {
2103 use azul_core::task::{Duration, SystemTimeDiff};
2104 use crate::timer::{Timer, TimerCallback};
2105 use azul_core::refany::RefAny;
2106
2107 Timer {
2108 refany: RefAny::new(()),
2109 node_id: None.into(),
2110 created: azul_core::task::Instant::now(),
2111 run_count: 0,
2112 last_run: azul_core::task::OptionInstant::None,
2113 delay: azul_core::task::OptionDuration::Some(Duration::System(
2114 SystemTimeDiff::from_millis(hover_time_ms as u64),
2115 )),
2116 interval: azul_core::task::OptionDuration::None,
2117 timeout: azul_core::task::OptionDuration::None,
2118 callback: TimerCallback::create(tooltip_delay_timer_callback),
2119 }
2120 }
2121
2122 pub fn handle_hover_change_for_tooltip(&self, hover_time_ms: u32) -> TooltipTimerAction {
2137 let current_hover = self.hover_manager.current_hover_node();
2138 let previous_hover = self.hover_manager.previous_hover_node();
2139
2140 if current_hover == previous_hover {
2141 return TooltipTimerAction::NoChange;
2142 }
2143
2144 let dom_id = DomId { inner: 0 };
2145 let Some(layout_result) = self.layout_results.get(&dom_id) else {
2146 return TooltipTimerAction::Stop;
2147 };
2148 let node_data_cont = layout_result.styled_dom.node_data.as_container();
2149
2150 let node_has_tooltip = |node_id: NodeId| -> bool {
2151 node_data_cont
2152 .get(node_id)
2153 .map(|n| n.get_accessible_label().is_some())
2154 .unwrap_or(false)
2155 };
2156
2157 match current_hover {
2158 Some(node) if node_has_tooltip(node) => {
2159 TooltipTimerAction::Start(self.create_tooltip_delay_timer(hover_time_ms))
2160 }
2161 _ => TooltipTimerAction::Stop,
2162 }
2163 }
2164
2165 fn is_node_contenteditable_internal(&self, dom_id: DomId, node_id: NodeId) -> bool {
2167 use crate::solver3::getters::is_node_contenteditable;
2168
2169 let Some(layout_result) = self.layout_results.get(&dom_id) else {
2170 return false;
2171 };
2172
2173 is_node_contenteditable(&layout_result.styled_dom, node_id)
2174 }
2175
2176 fn is_node_contenteditable_inherited_internal(&self, dom_id: DomId, node_id: NodeId) -> bool {
2182 use crate::solver3::getters::is_node_contenteditable_inherited;
2183
2184 let Some(layout_result) = self.layout_results.get(&dom_id) else {
2185 return false;
2186 };
2187
2188 is_node_contenteditable_inherited(&layout_result.styled_dom, node_id)
2189 }
2190
2191 pub fn handle_focus_change_for_cursor_blink(
2211 &mut self,
2212 new_focus: Option<azul_core::dom::DomNodeId>,
2213 current_window_state: &FullWindowState,
2214 ) -> CursorBlinkTimerAction {
2215 let contenteditable_info = match new_focus {
2218 Some(focus_node) => {
2219 if let Some(node_id) = focus_node.node.into_crate_internal() {
2220 if self.is_node_contenteditable_inherited_internal(focus_node.dom, node_id) {
2222 let text_node_id = self.find_last_text_child(focus_node.dom, node_id)
2224 .unwrap_or(node_id);
2225 Some((focus_node.dom, node_id, text_node_id))
2226 } else {
2227 None
2228 }
2229 } else {
2230 None
2231 }
2232 }
2233 None => None,
2234 };
2235
2236 let timer_was_active = self.text_edit_manager.blink.is_blink_timer_active();
2238
2239 if let Some((dom_id, container_node_id, text_node_id)) = contenteditable_info {
2240
2241 self.focus_manager.set_pending_contenteditable_focus(
2244 dom_id,
2245 container_node_id,
2246 text_node_id,
2247 );
2248
2249 let now = azul_core::task::Instant::now();
2251 self.text_edit_manager.blink.reset_blink_on_input(now);
2252 self.text_edit_manager.blink.set_blink_timer_active(true);
2253
2254 if !timer_was_active {
2255 let timer = self.create_cursor_blink_timer(current_window_state);
2257 return CursorBlinkTimerAction::Start(timer);
2258 } else {
2259 return CursorBlinkTimerAction::NoChange;
2261 }
2262 } else {
2263 self.text_edit_manager.clear_editing();
2267 self.focus_manager.clear_pending_contenteditable_focus();
2268
2269 if timer_was_active {
2270 self.text_edit_manager.blink.set_blink_timer_active(false);
2272 return CursorBlinkTimerAction::Stop;
2273 } else {
2274 return CursorBlinkTimerAction::NoChange;
2275 }
2276 }
2277 }
2278
2279 pub fn finalize_pending_focus_changes(&mut self) -> bool {
2301 let pending = match self.focus_manager.take_pending_contenteditable_focus() {
2303 Some(p) => p,
2304 None => return false,
2305 };
2306
2307 if self.text_edit_manager.multi_cursor.as_ref().map(|mc| mc.node_id.dom == pending.dom_id && mc.node_id.node.into_crate_internal() == Some(pending.text_node_id)).unwrap_or(false)
2312 || self.text_edit_manager.multi_cursor.as_ref().map(|mc| mc.node_id.dom == pending.dom_id && mc.node_id.node.into_crate_internal() == Some(pending.container_node_id)).unwrap_or(false)
2313 {
2314 return true;
2315 }
2316
2317 let text_layout = self.get_inline_layout_for_node(pending.dom_id, pending.text_node_id).cloned();
2319
2320 let cursor = text_layout.as_ref()
2323 .and_then(|layout| {
2324 layout.items.iter().rev()
2325 .find_map(|item| if let crate::text3::cache::ShapedItem::Cluster(c) = &item.item {
2326 Some(azul_core::selection::TextCursor {
2327 cluster_id: c.source_cluster_id,
2328 affinity: azul_core::selection::CursorAffinity::Trailing,
2329 })
2330 } else { None })
2331 })
2332 .unwrap_or(azul_core::selection::TextCursor {
2333 cluster_id: azul_core::selection::GraphemeClusterId { source_run: 0, start_byte_in_run: 0 },
2334 affinity: azul_core::selection::CursorAffinity::Trailing,
2335 });
2336 self.text_edit_manager.initialize_editing(cursor, pending.dom_id, pending.text_node_id, 0);
2337 true
2338 }
2339
2340 pub fn get_inline_layout_for_node(
2350 &self,
2351 dom_id: DomId,
2352 node_id: NodeId,
2353 ) -> Option<&Arc<UnifiedLayout>> {
2354 let layout_result = self.layout_results.get(&dom_id)?;
2355
2356 let layout_indices = layout_result.layout_tree.dom_to_layout.get(&node_id)?;
2357 let layout_index = *layout_indices.first()?;
2358
2359 layout_result.layout_tree.get_inline_layout_for_node(layout_index)
2361 }
2362
2363 fn resolve_step_static(
2365 layout: &crate::text3::cache::UnifiedLayout,
2366 cursor: &TextCursor,
2367 direction: azul_core::events::SelectionDirection,
2368 step: azul_core::events::SelectionStep,
2369 ) -> TextCursor {
2370 use azul_core::events::{SelectionDirection as D, SelectionStep as S};
2371 match (direction, step) {
2372 (D::Backward, S::Character) => layout.move_cursor_left(*cursor, &mut None),
2373 (D::Forward, S::Character) => layout.move_cursor_right(*cursor, &mut None),
2374 (D::Backward, S::Word) => layout.move_cursor_to_prev_word(*cursor, &mut None),
2375 (D::Forward, S::Word) => layout.move_cursor_to_next_word(*cursor, &mut None),
2376 (D::Backward, S::VisualLine) => layout.move_cursor_up(*cursor, &mut None, &mut None),
2377 (D::Forward, S::VisualLine) => layout.move_cursor_down(*cursor, &mut None, &mut None),
2378 (D::Backward, S::Line) => layout.move_cursor_to_line_start(*cursor, &mut None),
2379 (D::Forward, S::Line) => layout.move_cursor_to_line_end(*cursor, &mut None),
2380 (D::Backward, S::Document) => layout.get_first_cluster_cursor().unwrap_or(*cursor),
2381 (D::Forward, S::Document) => layout.get_last_cluster_cursor().unwrap_or(*cursor),
2382 }
2383 }
2384
2385 pub fn apply_selection_op(
2391 &mut self,
2392 target: azul_core::dom::DomNodeId,
2393 op: &azul_core::events::SelectionOp,
2394 ) -> bool {
2395 use azul_core::events::{SelectionMode, SelectionStep, SelectionDirection};
2396
2397 let dom_id = target.dom;
2398 let node_id = match target.node.into_crate_internal() {
2399 Some(id) => id,
2400 None => return false,
2401 };
2402
2403 let layout = match self.get_inline_layout_for_node(dom_id, node_id) {
2404 Some(l) => l.clone(),
2405 None => return false,
2406 };
2407
2408 match op.mode {
2409 SelectionMode::Move | SelectionMode::Extend => {
2410 let extend = matches!(op.mode, SelectionMode::Extend);
2411 if let Some(ref mut mc) = self.text_edit_manager.multi_cursor {
2412 for _ in 0..op.repeat.max(1) {
2413 mc.move_all_cursors(extend, |c| {
2414 Self::resolve_step_static(&layout, c, op.direction, op.step)
2415 });
2416 }
2417 }
2418 self.regenerate_display_list_for_dom(dom_id);
2419 true
2420 }
2421 SelectionMode::Delete => {
2422 if !matches!(op.step, SelectionStep::Character) {
2424 if let Some(ref mut mc) = self.text_edit_manager.multi_cursor {
2425 for _ in 0..op.repeat.max(1) {
2426 mc.move_all_cursors(true, |c| {
2427 Self::resolve_step_static(&layout, c, op.direction, op.step)
2428 });
2429 }
2430 }
2431 }
2432 let forward = matches!(op.direction, SelectionDirection::Forward);
2434 self.delete_selection(target, forward).is_some()
2435 }
2436 }
2437 }
2438
2439 pub fn move_cursor_in_node<F>(
2441 &self,
2442 dom_id: DomId,
2443 node_id: NodeId,
2444 movement_fn: F,
2445 ) -> Option<TextCursor>
2446 where
2447 F: FnOnce(&UnifiedLayout, &TextCursor) -> TextCursor,
2448 {
2449 let current_cursor = self.text_edit_manager.get_primary_cursor()?;
2450 let layout = self.get_inline_layout_for_node(dom_id, node_id)?;
2451
2452 let new_cursor = movement_fn(layout, ¤t_cursor);
2453
2454 if new_cursor != current_cursor {
2456 Some(new_cursor)
2457 } else {
2458 None
2459 }
2460 }
2461
2462 pub fn handle_cursor_movement(
2469 &mut self,
2470 dom_id: DomId,
2471 node_id: NodeId,
2472 new_cursor: TextCursor,
2473 extend_selection: bool,
2474 ) {
2475 if let Some(ref mut mc) = self.text_edit_manager.multi_cursor {
2477 mc.set_single_cursor(new_cursor);
2478 }
2479
2480 self.regenerate_display_list_for_dom(dom_id);
2481 }
2482
2483 pub fn handle_multi_cursor_movement(
2486 &mut self,
2487 dom_id: DomId,
2488 node_id: NodeId,
2489 extend_selection: bool,
2490 move_fn: impl Fn(&TextCursor) -> TextCursor,
2491 ) {
2492 if let Some(ref mut mc) = self.text_edit_manager.multi_cursor {
2493 mc.move_all_cursors(extend_selection, &move_fn);
2494 } else {
2495 if let Some(cursor) = self.text_edit_manager.get_primary_cursor() {
2497 let new_cursor = move_fn(&cursor);
2498 self.handle_cursor_movement(dom_id, node_id, new_cursor, extend_selection);
2499 return;
2500 }
2501 }
2502
2503 self.regenerate_display_list_for_dom(dom_id);
2504 }
2505
2506 pub fn get_gpu_cache(&self, dom_id: &DomId) -> Option<&GpuValueCache> {
2510 self.gpu_state_manager.caches.get(dom_id)
2511 }
2512
2513 pub fn get_gpu_cache_mut(&mut self, dom_id: &DomId) -> Option<&mut GpuValueCache> {
2515 self.gpu_state_manager.caches.get_mut(dom_id)
2516 }
2517
2518 pub fn get_or_create_gpu_cache(&mut self, dom_id: DomId) -> &mut GpuValueCache {
2520 self.gpu_state_manager.get_or_create_cache(dom_id)
2521 }
2522
2523 pub fn get_layout_result(&self, dom_id: &DomId) -> Option<&DomLayoutResult> {
2527 self.layout_results.get(dom_id)
2528 }
2529
2530 pub fn get_layout_result_mut(&mut self, dom_id: &DomId) -> Option<&mut DomLayoutResult> {
2532 self.layout_results.get_mut(dom_id)
2533 }
2534
2535 pub fn get_dom_ids(&self) -> DomIdVec {
2537 self.layout_results
2538 .keys()
2539 .copied()
2540 .collect::<Vec<_>>()
2541 .into()
2542 }
2543
2544 pub fn compute_cursor_type_hit_test(
2551 &self,
2552 hit_test: &crate::hit_test::FullHitTest,
2553 ) -> crate::hit_test::CursorTypeHitTest {
2554 crate::hit_test::CursorTypeHitTest::new(hit_test, self)
2555 }
2556
2557 fn calculate_scrollbar_opacity(
2577 last_activity: Option<Instant>,
2578 now: Instant,
2579 fade_delay: Duration,
2580 fade_duration: Duration,
2581 ) -> f32 {
2582 let Some(last_activity) = last_activity else {
2583 return 0.0;
2584 };
2585
2586 let time_since_activity = now.duration_since(&last_activity);
2587
2588 if time_since_activity.div(&fade_delay) < 1.0 {
2590 return 1.0;
2591 }
2592
2593 let time_into_fade = time_since_activity.div(&fade_delay) - 1.0;
2595 let fade_progress = (time_into_fade * fade_delay.div(&fade_duration)).min(1.0);
2596
2597 (1.0 - fade_progress).max(0.0)
2599 }
2600
2601 pub fn synchronize_scrollbar_opacity(
2606 gpu_state_manager: &mut GpuStateManager,
2607 scroll_manager: &ScrollManager,
2608 dom_id: DomId,
2609 layout_tree: &LayoutTree,
2610 system_callbacks: &ExternalSystemCallbacks,
2611 fade_delay: azul_core::task::Duration,
2612 fade_duration: azul_core::task::Duration,
2613 ) -> Vec<azul_core::gpu::GpuScrollbarOpacityEvent> {
2614 let mut events = Vec::new();
2615 let mut any_opacity_nonzero = false;
2616 let gpu_cache = gpu_state_manager.caches.entry(dom_id).or_default();
2617
2618 let now = (system_callbacks.get_system_time_fn.cb)();
2620
2621 for (node_idx, node) in layout_tree.nodes.iter().enumerate() {
2623 let warm = layout_tree.warm(node_idx);
2625 let scrollbar_info = match warm.and_then(|w| w.scrollbar_info.as_ref()) {
2626 Some(info) => info,
2627 None => continue,
2628 };
2629
2630 let node_id = match node.dom_node_id {
2631 Some(nid) => nid,
2632 None => continue, };
2634
2635 let vertical_opacity = if scrollbar_info.needs_vertical {
2637 Self::calculate_scrollbar_opacity(
2638 scroll_manager.get_last_activity_time(dom_id, node_id),
2639 now.clone(),
2640 fade_delay,
2641 fade_duration,
2642 )
2643 } else {
2644 0.0
2645 };
2646
2647 let horizontal_opacity = if scrollbar_info.needs_horizontal {
2648 Self::calculate_scrollbar_opacity(
2649 scroll_manager.get_last_activity_time(dom_id, node_id),
2650 now.clone(),
2651 fade_delay,
2652 fade_duration,
2653 )
2654 } else {
2655 0.0
2656 };
2657
2658 if (vertical_opacity > 0.0 && vertical_opacity < 1.0)
2664 || (horizontal_opacity > 0.0 && horizontal_opacity < 1.0)
2665 {
2666 any_opacity_nonzero = true;
2667 }
2668
2669 if scrollbar_info.needs_vertical {
2678 let key = (dom_id, node_id);
2679 let existing = gpu_cache.scrollbar_v_opacity_values.get(&key);
2680
2681 match existing {
2682 None => {
2683 let opacity_key = OpacityKey::unique();
2684 gpu_cache.scrollbar_v_opacity_keys.insert(key, opacity_key);
2685 gpu_cache
2686 .scrollbar_v_opacity_values
2687 .insert(key, vertical_opacity);
2688 events.push(GpuScrollbarOpacityEvent::VerticalAdded(
2689 dom_id,
2690 node_id,
2691 opacity_key,
2692 vertical_opacity,
2693 ));
2694 }
2695 Some(&old_opacity) if (old_opacity - vertical_opacity).abs() > 0.001 => {
2696 let opacity_key = gpu_cache.scrollbar_v_opacity_keys[&key];
2697 gpu_cache
2698 .scrollbar_v_opacity_values
2699 .insert(key, vertical_opacity);
2700 events.push(GpuScrollbarOpacityEvent::VerticalChanged(
2701 dom_id,
2702 node_id,
2703 opacity_key,
2704 old_opacity,
2705 vertical_opacity,
2706 ));
2707 }
2708 _ => {}
2709 }
2710 } else {
2711 let key = (dom_id, node_id);
2713 if let Some(opacity_key) = gpu_cache.scrollbar_v_opacity_keys.remove(&key) {
2714 gpu_cache.scrollbar_v_opacity_values.remove(&key);
2715 events.push(GpuScrollbarOpacityEvent::VerticalRemoved(
2716 dom_id,
2717 node_id,
2718 opacity_key,
2719 ));
2720 }
2721 }
2722
2723 if scrollbar_info.needs_horizontal {
2725 let key = (dom_id, node_id);
2726 let existing = gpu_cache.scrollbar_h_opacity_values.get(&key);
2727
2728 match existing {
2729 None => {
2730 let opacity_key = OpacityKey::unique();
2731 gpu_cache.scrollbar_h_opacity_keys.insert(key, opacity_key);
2732 gpu_cache
2733 .scrollbar_h_opacity_values
2734 .insert(key, horizontal_opacity);
2735 events.push(GpuScrollbarOpacityEvent::HorizontalAdded(
2736 dom_id,
2737 node_id,
2738 opacity_key,
2739 horizontal_opacity,
2740 ));
2741 }
2742 Some(&old_opacity) if (old_opacity - horizontal_opacity).abs() > 0.001 => {
2743 let opacity_key = gpu_cache.scrollbar_h_opacity_keys[&key];
2744 gpu_cache
2745 .scrollbar_h_opacity_values
2746 .insert(key, horizontal_opacity);
2747 events.push(GpuScrollbarOpacityEvent::HorizontalChanged(
2748 dom_id,
2749 node_id,
2750 opacity_key,
2751 old_opacity,
2752 horizontal_opacity,
2753 ));
2754 }
2755 _ => {}
2756 }
2757 } else {
2758 let key = (dom_id, node_id);
2760 if let Some(opacity_key) = gpu_cache.scrollbar_h_opacity_keys.remove(&key) {
2761 gpu_cache.scrollbar_h_opacity_values.remove(&key);
2762 events.push(GpuScrollbarOpacityEvent::HorizontalRemoved(
2763 dom_id,
2764 node_id,
2765 opacity_key,
2766 ));
2767 }
2768 }
2769 }
2770
2771 if any_opacity_nonzero {
2775 gpu_state_manager.scrollbar_fade_active = true;
2776 } else {
2777 gpu_state_manager.scrollbar_fade_active = false;
2778 }
2779
2780 events
2781 }
2782
2783 pub fn compute_scroll_ids(
2792 layout_tree: &LayoutTree,
2793 styled_dom: &azul_core::styled_dom::StyledDom,
2794 ) -> (HashMap<usize, u64>, HashMap<u64, NodeId>) {
2795 use azul_css::props::layout::LayoutOverflow;
2796
2797 use crate::solver3::getters::{get_overflow_x, get_overflow_y};
2798
2799 let mut scroll_ids = HashMap::new();
2800 let mut scroll_id_to_node_id = HashMap::new();
2801
2802 for (layout_idx, node) in layout_tree.nodes.iter().enumerate() {
2804 let Some(dom_node_id) = node.dom_node_id else {
2805 continue;
2806 };
2807
2808 let styled_node_state = styled_dom
2810 .styled_nodes
2811 .as_container()
2812 .get(dom_node_id)
2813 .map(|n| n.styled_node_state.clone())
2814 .unwrap_or_default();
2815
2816 let overflow_x = get_overflow_x(styled_dom, dom_node_id, &styled_node_state);
2818 let overflow_y = get_overflow_y(styled_dom, dom_node_id, &styled_node_state);
2819
2820 let is_scrollable = overflow_x.is_scroll() || overflow_y.is_scroll();
2821
2822 if !is_scrollable {
2823 continue;
2824 }
2825
2826 let scroll_id = {
2829 use std::hash::{Hash, Hasher, DefaultHasher};
2830 let mut h = DefaultHasher::new();
2831 if let Some(cold) = layout_tree.cold(layout_idx) {
2832 cold.node_data_fingerprint.hash(&mut h);
2833 }
2834 h.finish()
2835 };
2836
2837 scroll_ids.insert(layout_idx, scroll_id);
2838 scroll_id_to_node_id.insert(scroll_id, dom_node_id);
2839 }
2840
2841 (scroll_ids, scroll_id_to_node_id)
2842 }
2843
2844 pub fn get_node_layout_rect(
2851 &self,
2852 node_id: azul_core::dom::DomNodeId,
2853 ) -> Option<azul_core::geom::LogicalRect> {
2854 let layout_tree = self.layout_cache.tree.as_ref()?;
2856 unsafe { core::ptr::write_volatile(0x400E8 as *mut u32, 0xE5_000002u32 | ((layout_tree.nodes.len() as u32 & 0xff) << 8)); }
2858
2859 let target_node_id = node_id.node.into_crate_internal();
2862 let layout_idx = match layout_tree.nodes.iter().position(|node| node.dom_node_id == target_node_id) {
2863 Some(i) => i,
2864 None => { unsafe { core::ptr::write_volatile(0x400E8 as *mut u32, 0xE5_0000FFu32); } return None; }
2865 };
2866 unsafe { core::ptr::write_volatile(0x400E8 as *mut u32, 0xE5_000003u32 | ((self.layout_cache.calculated_positions.len() as u32 & 0xfff) << 8)); }
2867
2868 let calc_pos = match self.layout_cache.calculated_positions.get(layout_idx) {
2870 Some(p) => p,
2871 None => { unsafe { core::ptr::write_volatile(0x400E8 as *mut u32, 0xE5_0000FEu32); } return None; }
2872 };
2873
2874 let layout_node = layout_tree.nodes.get(layout_idx)?;
2876
2877 let used_size = match layout_node.used_size {
2879 Some(s) => s,
2880 None => { unsafe { core::ptr::write_volatile(0x400E8 as *mut u32, 0xE5_0000FDu32); } return None; }
2881 };
2882 unsafe { core::ptr::write_volatile(0x400E8 as *mut u32, 0xE5_000004u32); }
2883
2884 let hidpi_factor = self
2886 .current_window_state
2887 .size
2888 .get_hidpi_factor()
2889 .inner
2890 .get();
2891
2892 Some(LogicalRect::new(
2893 LogicalPosition::new(calc_pos.x as f32, calc_pos.y as f32),
2894 LogicalSize::new(
2895 used_size.width / hidpi_factor,
2896 used_size.height / hidpi_factor,
2897 ),
2898 ))
2899 }
2900
2901 #[cfg(feature = "a11y")]
2920 pub fn update_a11y_tree(&mut self) {
2921 let cursor_a11y_info = self.text_edit_manager.multi_cursor.as_ref().and_then(|mc| {
2922 let node_id = mc.node_id.node.into_crate_internal()?;
2923 let primary = mc.get_primary()?;
2924 let (anchor_offset, focus_offset) = match &primary.selection {
2925 azul_core::selection::Selection::Cursor(c) => {
2926 let off = c.cluster_id.start_byte_in_run as usize;
2927 (off, off)
2928 }
2929 azul_core::selection::Selection::Range(r) => (
2930 r.start.cluster_id.start_byte_in_run as usize,
2931 r.end.cluster_id.start_byte_in_run as usize,
2932 ),
2933 };
2934 Some(crate::managers::a11y::CursorA11yInfo {
2935 dom_id: mc.node_id.dom,
2936 node_id,
2937 anchor_offset,
2938 focus_offset,
2939 })
2940 });
2941
2942 let mut dirty_text_overrides: BTreeMap<(DomId, NodeId), String> = BTreeMap::new();
2945 for (&(dom_id, node_id), dirty_node) in &self.dirty_text_nodes {
2946 dirty_text_overrides.insert(
2947 (dom_id, node_id),
2948 self.extract_text_from_inline_content(&dirty_node.content),
2949 );
2950 }
2951
2952 let a11y_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2953 crate::managers::a11y::A11yManager::update_tree(
2954 self.a11y_manager.root_id,
2955 &self.layout_results,
2956 &self.current_window_state.title,
2957 self.current_window_state.size.dimensions,
2958 self.focus_manager.get_focused_node().copied(),
2959 self.current_window_state.size.get_hidpi_factor().inner.get(),
2960 &dirty_text_overrides,
2961 cursor_a11y_info,
2962 )
2963 }));
2964
2965 match a11y_result {
2966 Ok(tree_update) => {
2967 self.a11y_manager.last_tree_update = Some(tree_update);
2968 self.a11y_manager.tree_initialized = true;
2969 }
2970 Err(_) => {}
2971 }
2972 }
2973
2974 #[cfg(feature = "a11y")]
2978 pub fn update_a11y_tree_incremental(&mut self) {
2979 if !self.a11y_manager.tree_initialized {
2980 return self.update_a11y_tree();
2982 }
2983
2984 let mc = match self.text_edit_manager.multi_cursor.as_ref() {
2986 Some(mc) => mc,
2987 None => return, };
2989
2990 let dom_node_id = mc.node_id;
2991 let node_id = match dom_node_id.node.into_crate_internal() {
2992 Some(id) => id,
2993 None => return,
2994 };
2995 let dom_id = dom_node_id.dom;
2996
2997 let text_content = if let Some(dirty) = self.dirty_text_nodes.get(&(dom_id, node_id)) {
2999 self.extract_text_from_inline_content(&dirty.content)
3000 } else {
3001 let lr = match self.layout_results.get(&dom_id) {
3003 Some(lr) => lr,
3004 None => return self.update_a11y_tree(),
3005 };
3006 let node_data = lr.styled_dom.node_data.as_ref();
3007 let hierarchy = lr.styled_dom.node_hierarchy.as_ref();
3008 let mut text = String::new();
3009 if let Some(item) = hierarchy.get(node_id.index()) {
3010 let mut child = item.first_child_id(node_id);
3011 while let Some(child_id) = child {
3012 if let Some(cd) = node_data.get(child_id.index()) {
3013 if let azul_core::dom::NodeType::Text(t) = &cd.node_type {
3014 if !text.is_empty() { text.push(' '); }
3015 text.push_str(t.as_str());
3016 }
3017 }
3018 if child_id.index() >= hierarchy.len() { break; }
3019 child = hierarchy[child_id.index()].next_sibling_id();
3020 }
3021 }
3022 text
3023 };
3024
3025 let a11y_node_id = accesskit::NodeId(
3027 ((dom_id.inner as u64) << 32) | (node_id.index() as u64) + 1,
3028 );
3029
3030 let role = self.layout_results.get(&dom_id)
3032 .and_then(|lr| lr.styled_dom.node_data.as_ref().get(node_id.index()))
3033 .map(|nd| {
3034 if nd.is_contenteditable() || matches!(nd.node_type, azul_core::dom::NodeType::TextArea) {
3035 accesskit::Role::MultilineTextInput
3036 } else if matches!(nd.node_type, azul_core::dom::NodeType::Input) {
3037 accesskit::Role::TextInput
3038 } else {
3039 accesskit::Role::GenericContainer
3040 }
3041 })
3042 .unwrap_or(accesskit::Role::GenericContainer);
3043
3044 let mut node = accesskit::Node::new(role);
3045 node.set_value(text_content.as_str());
3046 node.add_action(accesskit::Action::SetTextSelection);
3047 node.add_action(accesskit::Action::ReplaceSelectedText);
3048 node.add_action(accesskit::Action::SetValue);
3049
3050 let primary = mc.get_primary();
3052 if let Some(identified) = primary {
3053 let (anchor_off, focus_off) = match &identified.selection {
3054 azul_core::selection::Selection::Cursor(c) => {
3055 let off = c.cluster_id.start_byte_in_run as usize;
3056 (off, off)
3057 }
3058 azul_core::selection::Selection::Range(r) => (
3059 r.start.cluster_id.start_byte_in_run as usize,
3060 r.end.cluster_id.start_byte_in_run as usize,
3061 ),
3062 };
3063
3064 let char_lengths: Vec<u8> = text_content.chars()
3065 .map(|c| c.len_utf16() as u8)
3066 .collect();
3067 node.set_character_lengths(char_lengths.clone());
3068
3069 let byte_to_char = |byte_off: usize| -> usize {
3070 text_content.char_indices()
3071 .take_while(|(b, _)| *b < byte_off)
3072 .count()
3073 .min(char_lengths.len())
3074 };
3075
3076 node.set_text_selection(accesskit::TextSelection {
3077 anchor: accesskit::TextPosition {
3078 node: a11y_node_id,
3079 character_index: byte_to_char(anchor_off),
3080 },
3081 focus: accesskit::TextPosition {
3082 node: a11y_node_id,
3083 character_index: byte_to_char(focus_off),
3084 },
3085 });
3086 }
3087
3088 let focus = self.focus_manager.get_focused_node().copied()
3090 .and_then(|dn| {
3091 let idx = dn.node.into_crate_internal()?.index();
3092 Some(accesskit::NodeId(((dn.dom.inner as u64) << 32) | (idx as u64) + 1))
3093 })
3094 .unwrap_or(self.a11y_manager.root_id);
3095
3096 self.a11y_manager.last_tree_update = Some(accesskit::TreeUpdate {
3097 nodes: vec![(a11y_node_id, node)],
3098 tree: None, focus,
3100 tree_id: accesskit::TreeId::ROOT,
3101 });
3102 }
3103
3104 pub fn get_focused_cursor_rect(&self) -> Option<azul_core::geom::LogicalRect> {
3105 let focused_node = self.focus_manager.focused_node?;
3107
3108 let cursor = self.text_edit_manager.get_primary_cursor()?;
3110
3111 let layout_tree = self.layout_cache.tree.as_ref()?;
3113
3114 let target_node_id = focused_node.node.into_crate_internal();
3116 let layout_idx = layout_tree
3117 .nodes
3118 .iter()
3119 .position(|node| node.dom_node_id == target_node_id)?;
3120
3121 let warm_node = layout_tree.warm(layout_idx)?;
3123 let cached_layout = warm_node.inline_layout_result.as_ref()?;
3124 let inline_layout = &cached_layout.layout;
3125
3126 let mut cursor_rect = inline_layout.get_cursor_rect(&cursor)?;
3128
3129 let calc_pos = self.layout_cache.calculated_positions.get(layout_idx)?;
3131
3132 cursor_rect.origin.x += calc_pos.x as f32;
3134 cursor_rect.origin.y += calc_pos.y as f32;
3135
3136 Some(cursor_rect)
3138 }
3139
3140 pub fn calculate_selection_bounding_rect(&self) -> Option<azul_core::geom::LogicalRect> {
3143 let focused_node = self.focus_manager.focused_node?;
3144 let mc = self.text_edit_manager.multi_cursor.as_ref()?;
3145
3146 let ranges: alloc::vec::Vec<_> = mc.selections.iter().filter_map(|s| {
3148 if let azul_core::selection::Selection::Range(ref r) = s.selection {
3149 Some(r.clone())
3150 } else {
3151 None
3152 }
3153 }).collect();
3154
3155 if ranges.is_empty() {
3156 return None;
3157 }
3158
3159 let target_node_id = focused_node.node.into_crate_internal();
3161 let layout_tree = self.layout_cache.tree.as_ref()?;
3162 let layout_idx = layout_tree.nodes.iter()
3163 .position(|n| n.dom_node_id == target_node_id)?;
3164 let warm = layout_tree.warm(layout_idx)?;
3165 let inline_layout = &warm.inline_layout_result.as_ref()?.layout;
3166 let calc_pos = self.layout_cache.calculated_positions.get(layout_idx)?;
3167
3168 let mut min_x = f32::MAX;
3169 let mut min_y = f32::MAX;
3170 let mut max_x = f32::MIN;
3171 let mut max_y = f32::MIN;
3172 let mut found_any = false;
3173
3174 for range in &ranges {
3175 for rect in inline_layout.get_selection_rects(range) {
3176 found_any = true;
3177 let abs_x = rect.origin.x + calc_pos.x as f32;
3178 let abs_y = rect.origin.y + calc_pos.y as f32;
3179 min_x = min_x.min(abs_x);
3180 min_y = min_y.min(abs_y);
3181 max_x = max_x.max(abs_x + rect.size.width);
3182 max_y = max_y.max(abs_y + rect.size.height);
3183 }
3184 }
3185
3186 if !found_any {
3187 return None;
3188 }
3189
3190 Some(LogicalRect::new(
3191 LogicalPosition { x: min_x, y: min_y },
3192 LogicalSize { width: max_x - min_x, height: max_y - min_y },
3193 ))
3194 }
3195
3196 pub fn select_next_occurrence(&mut self) -> bool {
3204 use crate::text3::selection::select_word_at_cursor;
3205
3206 let mc = match self.text_edit_manager.multi_cursor.as_mut() {
3207 Some(mc) => mc,
3208 None => return false,
3209 };
3210 let node_id = mc.node_id;
3211 let dom_node_id = match node_id.node.into_crate_internal() {
3212 Some(id) => id,
3213 None => return false,
3214 };
3215
3216 let primary = match mc.selections.first() {
3218 Some(s) => s.clone(),
3219 None => return false,
3220 };
3221
3222 let (search_range, need_word_expand) = match &primary.selection {
3223 azul_core::selection::Selection::Range(r) => (*r, false),
3224 azul_core::selection::Selection::Cursor(c) => {
3225 (azul_core::selection::SelectionRange { start: c.clone(), end: c.clone() }, true)
3227 }
3228 };
3229
3230 let inline_layout = match self.get_node_inline_layout(node_id.dom, dom_node_id) {
3232 Some(l) => l,
3233 None => return false,
3234 };
3235
3236 let word_range = if need_word_expand {
3238 match select_word_at_cursor(&search_range.start, &inline_layout) {
3239 Some(r) => r,
3240 None => return false,
3241 }
3242 } else {
3243 search_range
3244 };
3245
3246 let content = self.get_text_before_textinput(node_id.dom, dom_node_id);
3248 let full_text = self.extract_text_from_inline_content(&content);
3249
3250 let start_byte = word_range.start.cluster_id.start_byte_in_run as usize;
3252 let end_byte = word_range.end.cluster_id.start_byte_in_run as usize;
3253 let search_text = if word_range.start.cluster_id.source_run == word_range.end.cluster_id.source_run {
3254 if let Some(InlineContent::Text(run)) = content.get(word_range.start.cluster_id.source_run as usize) {
3255 if start_byte <= end_byte && end_byte <= run.text.len() {
3256 run.text[start_byte..end_byte].to_string()
3257 } else {
3258 return false;
3259 }
3260 } else {
3261 return false;
3262 }
3263 } else {
3264 return false; };
3266
3267 if search_text.is_empty() {
3268 return false;
3269 }
3270
3271 let mc = self.text_edit_manager.multi_cursor.as_ref().unwrap();
3273 let last_end_byte = mc.selections.last()
3274 .and_then(|s| match &s.selection {
3275 azul_core::selection::Selection::Range(r) => Some(r.end.cluster_id.start_byte_in_run as usize),
3276 azul_core::selection::Selection::Cursor(c) => Some(c.cluster_id.start_byte_in_run as usize),
3277 })
3278 .unwrap_or(0);
3279
3280 let search_run = word_range.start.cluster_id.source_run;
3281
3282 if let Some(InlineContent::Text(run)) = content.get(search_run as usize) {
3284 let search_in = &run.text;
3285 if let Some(offset) = search_in[last_end_byte..].find(&search_text) {
3287 let match_start = last_end_byte + offset;
3288 let match_end = match_start + search_text.len();
3289
3290 let new_range = azul_core::selection::SelectionRange {
3291 start: azul_core::selection::TextCursor {
3292 cluster_id: azul_core::selection::GraphemeClusterId {
3293 source_run: search_run,
3294 start_byte_in_run: match_start as u32,
3295 },
3296 affinity: azul_core::selection::CursorAffinity::Leading,
3297 },
3298 end: azul_core::selection::TextCursor {
3299 cluster_id: azul_core::selection::GraphemeClusterId {
3300 source_run: search_run,
3301 start_byte_in_run: match_end as u32,
3302 },
3303 affinity: azul_core::selection::CursorAffinity::Trailing,
3304 },
3305 };
3306
3307 let mc = self.text_edit_manager.multi_cursor.as_mut().unwrap();
3309 if need_word_expand {
3310 if let Some(first) = mc.selections.first_mut() {
3311 first.selection = azul_core::selection::Selection::Range(word_range);
3312 }
3313 }
3314 let _ = mc.add_selection(new_range);
3315 self.text_edit_manager.mark_dirty();
3316 return true;
3317 } else if last_end_byte > 0 {
3318 if let Some(offset) = search_in[..start_byte].find(&search_text) {
3320 let match_start = offset;
3321 let match_end = match_start + search_text.len();
3322
3323 let new_range = azul_core::selection::SelectionRange {
3324 start: azul_core::selection::TextCursor {
3325 cluster_id: azul_core::selection::GraphemeClusterId {
3326 source_run: search_run,
3327 start_byte_in_run: match_start as u32,
3328 },
3329 affinity: azul_core::selection::CursorAffinity::Leading,
3330 },
3331 end: azul_core::selection::TextCursor {
3332 cluster_id: azul_core::selection::GraphemeClusterId {
3333 source_run: search_run,
3334 start_byte_in_run: match_end as u32,
3335 },
3336 affinity: azul_core::selection::CursorAffinity::Trailing,
3337 },
3338 };
3339
3340 let mc = self.text_edit_manager.multi_cursor.as_mut().unwrap();
3341 if need_word_expand {
3342 if let Some(first) = mc.selections.first_mut() {
3343 first.selection = azul_core::selection::Selection::Range(word_range);
3344 }
3345 }
3346 let _ = mc.add_selection(new_range);
3347 self.text_edit_manager.mark_dirty();
3348 return true;
3349 }
3350 }
3351 }
3352
3353 if need_word_expand {
3356 let mc = self.text_edit_manager.multi_cursor.as_mut().unwrap();
3357 if let Some(first) = mc.selections.first_mut() {
3358 first.selection = azul_core::selection::Selection::Range(word_range);
3359 }
3360 self.text_edit_manager.mark_dirty();
3361 return true;
3362 }
3363
3364 false
3365 }
3366
3367 pub fn get_focused_cursor_rect_viewport(&self) -> Option<azul_core::geom::LogicalRect> {
3385 let mut cursor_rect = self.get_focused_cursor_rect()?;
3387
3388 let focused_node = self.focus_manager.focused_node?;
3390
3391 let layout_tree = self.layout_cache.tree.as_ref()?;
3393
3394 let target_node_id = focused_node.node.into_crate_internal();
3396 let layout_idx = layout_tree
3397 .nodes
3398 .iter()
3399 .position(|node| node.dom_node_id == target_node_id)?;
3400
3401 let gpu_cache = self.gpu_state_manager.caches.get(&focused_node.dom);
3403
3404 let mut current_layout_idx = layout_idx;
3408
3409 while let Some(parent_idx) = layout_tree.nodes.get(current_layout_idx)?.parent {
3410 if let Some(parent_dom_node_id) = layout_tree.nodes.get(parent_idx)?.dom_node_id {
3412 if let Some(scroll_state) = self
3414 .scroll_manager
3415 .get_scroll_state(focused_node.dom, parent_dom_node_id)
3416 {
3417 cursor_rect.origin.x -= scroll_state.current_offset.x;
3419 cursor_rect.origin.y -= scroll_state.current_offset.y;
3420 }
3421
3422 if let Some(cache) = gpu_cache {
3424 if let Some(transform) = cache.current_transform_values.get(&parent_dom_node_id)
3425 {
3426 let inverse = transform.inverse();
3429 if let Some(transformed_origin) =
3430 inverse.transform_point2d(cursor_rect.origin)
3431 {
3432 cursor_rect.origin = transformed_origin;
3433 }
3434 }
3436 }
3437 }
3438
3439 current_layout_idx = parent_idx;
3441 }
3442
3443 Some(cursor_rect)
3444 }
3445
3446 pub fn find_scrollable_ancestor(
3450 &self,
3451 mut node_id: azul_core::dom::DomNodeId,
3452 ) -> Option<azul_core::dom::DomNodeId> {
3453 let layout_tree = self.layout_cache.tree.as_ref()?;
3455
3456 let mut current_node_id = node_id.node.into_crate_internal();
3458
3459 loop {
3461 let layout_idx = layout_tree
3463 .nodes
3464 .iter()
3465 .position(|node| node.dom_node_id == current_node_id)?;
3466
3467 if layout_tree.warm(layout_idx).and_then(|w| w.scrollbar_info.as_ref()).is_some() {
3469 let check_node_id = current_node_id?;
3471 if self
3472 .scroll_manager
3473 .get_scroll_state(node_id.dom, check_node_id)
3474 .is_some()
3475 {
3476 return Some(azul_core::dom::DomNodeId {
3478 dom: node_id.dom,
3479 node: azul_core::styled_dom::NodeHierarchyItemId::from_crate_internal(
3480 Some(check_node_id),
3481 ),
3482 });
3483 }
3484 }
3485
3486 let parent_idx = layout_tree.get(layout_idx)?.parent?;
3488 let parent_node = layout_tree.get(parent_idx)?;
3489 current_node_id = parent_node.dom_node_id;
3490 }
3491 }
3492
3493 pub fn scroll_selection_into_view(
3519 &mut self,
3520 scroll_type: SelectionScrollType,
3521 scroll_mode: ScrollMode,
3522 ) -> bool {
3523 let bounds = match scroll_type {
3525 SelectionScrollType::Cursor => {
3526 match self.get_focused_cursor_rect() {
3528 Some(rect) => rect,
3529 None => return false, }
3531 }
3532 SelectionScrollType::Selection => {
3533 match self.calculate_selection_bounding_rect()
3536 .or_else(|| self.get_focused_cursor_rect())
3537 {
3538 Some(rect) => rect,
3539 None => return false,
3540 }
3541 }
3542 SelectionScrollType::DragSelection { mouse_position } => {
3543 LogicalRect::new(mouse_position, LogicalSize::zero())
3545 }
3546 };
3547
3548 let focused_node = match self.focus_manager.focused_node {
3550 Some(node) => node,
3551 None => return false,
3552 };
3553
3554 let scroll_container = match self.find_scrollable_ancestor(focused_node) {
3556 Some(node) => node,
3557 None => return false, };
3559
3560 let layout_tree = match self.layout_cache.tree.as_ref() {
3562 Some(tree) => tree,
3563 None => return false,
3564 };
3565
3566 let scrollable_node_internal = match scroll_container.node.into_crate_internal() {
3567 Some(id) => id,
3568 None => return false,
3569 };
3570
3571 let layout_idx = match layout_tree
3572 .nodes
3573 .iter()
3574 .position(|n| n.dom_node_id == Some(scrollable_node_internal))
3575 {
3576 Some(idx) => idx,
3577 None => return false,
3578 };
3579
3580 let scrollable_layout_node = match layout_tree.nodes.get(layout_idx) {
3581 Some(node) => node,
3582 None => return false,
3583 };
3584
3585 let container_pos = self
3586 .layout_cache
3587 .calculated_positions
3588 .get(layout_idx)
3589 .copied()
3590 .unwrap_or_default();
3591
3592 let container_size = scrollable_layout_node.used_size.unwrap_or_default();
3593
3594 let container_rect = LogicalRect {
3595 origin: container_pos,
3596 size: container_size,
3597 };
3598
3599 let scroll_state = match self
3601 .scroll_manager
3602 .get_scroll_state(scroll_container.dom, scrollable_node_internal)
3603 {
3604 Some(state) => state,
3605 None => return false,
3606 };
3607
3608 let visible_area = LogicalRect::new(
3610 LogicalPosition::new(
3611 container_rect.origin.x + scroll_state.current_offset.x,
3612 container_rect.origin.y + scroll_state.current_offset.y,
3613 ),
3614 container_rect.size,
3615 );
3616
3617 let scroll_delta = match scroll_mode {
3619 ScrollMode::Instant => {
3620 calculate_instant_scroll_delta(bounds, visible_area)
3622 }
3623 ScrollMode::Accelerated => {
3624 let distance = calculate_edge_distance(bounds, visible_area);
3626 calculate_accelerated_scroll_delta(distance)
3627 }
3628 };
3629
3630 if scroll_delta.x != 0.0 || scroll_delta.y != 0.0 {
3632 let duration = match scroll_mode {
3633 ScrollMode::Instant => Duration::System(SystemTimeDiff { secs: 0, nanos: 0 }),
3634 ScrollMode::Accelerated => Duration::System(SystemTimeDiff {
3635 secs: 0,
3636 nanos: 16_666_667,
3637 }), };
3639
3640 let external = ExternalSystemCallbacks::rust_internal();
3641 let now = (external.get_system_time_fn.cb)();
3642
3643 let new_target = LogicalPosition {
3645 x: scroll_state.current_offset.x + scroll_delta.x,
3646 y: scroll_state.current_offset.y + scroll_delta.y,
3647 };
3648
3649 self.scroll_manager.scroll_to(
3650 scroll_container.dom,
3651 scrollable_node_internal,
3652 new_target,
3653 duration,
3654 EasingFunction::Linear,
3655 now.into(),
3656 );
3657
3658 true } else {
3660 false }
3662 }
3663
3664 fn scroll_focused_cursor_into_view(&mut self) {
3669 self.scroll_selection_into_view(SelectionScrollType::Cursor, ScrollMode::Instant);
3671 }
3672}
3673
3674#[derive(Debug, Clone, Copy)]
3676pub enum SelectionScrollType {
3677 Cursor,
3679 Selection,
3681 DragSelection { mouse_position: LogicalPosition },
3683}
3684
3685#[derive(Debug, Clone, Copy)]
3687pub enum ScrollMode {
3688 Instant,
3690 Accelerated,
3692}
3693
3694#[derive(Debug, Clone, Copy)]
3696struct EdgeDistance {
3697 left: f32,
3698 right: f32,
3699 top: f32,
3700 bottom: f32,
3701}
3702
3703fn calculate_edge_distance(rect: LogicalRect, container: LogicalRect) -> EdgeDistance {
3705 EdgeDistance {
3706 left: (rect.origin.x - container.origin.x).max(0.0),
3708 right: ((container.origin.x + container.size.width) - (rect.origin.x + rect.size.width))
3710 .max(0.0),
3711 top: (rect.origin.y - container.origin.y).max(0.0),
3713 bottom: ((container.origin.y + container.size.height) - (rect.origin.y + rect.size.height))
3715 .max(0.0),
3716 }
3717}
3718
3719fn calculate_instant_scroll_delta(
3721 bounds: LogicalRect,
3722 visible_area: LogicalRect,
3723) -> LogicalPosition {
3724 const PADDING: f32 = 5.0;
3725 let mut delta = LogicalPosition::zero();
3726
3727 if bounds.origin.x < visible_area.origin.x + PADDING {
3729 delta.x = bounds.origin.x - visible_area.origin.x - PADDING;
3730 } else if bounds.origin.x + bounds.size.width
3731 > visible_area.origin.x + visible_area.size.width - PADDING
3732 {
3733 delta.x = (bounds.origin.x + bounds.size.width)
3734 - (visible_area.origin.x + visible_area.size.width)
3735 + PADDING;
3736 }
3737
3738 if bounds.origin.y < visible_area.origin.y + PADDING {
3740 delta.y = bounds.origin.y - visible_area.origin.y - PADDING;
3741 } else if bounds.origin.y + bounds.size.height
3742 > visible_area.origin.y + visible_area.size.height - PADDING
3743 {
3744 delta.y = (bounds.origin.y + bounds.size.height)
3745 - (visible_area.origin.y + visible_area.size.height)
3746 + PADDING;
3747 }
3748
3749 delta
3750}
3751
3752fn calculate_accelerated_scroll_delta(distance: EdgeDistance) -> LogicalPosition {
3754 const DEAD_ZONE: f32 = 20.0;
3756 const SLOW_ZONE: f32 = 50.0;
3757 const MEDIUM_ZONE: f32 = 100.0;
3758 const FAST_ZONE: f32 = 200.0;
3759
3760 const SLOW_SPEED: f32 = 2.0;
3762 const MEDIUM_SPEED: f32 = 4.0;
3763 const FAST_SPEED: f32 = 8.0;
3764 const VERY_FAST_SPEED: f32 = 16.0;
3765
3766 let speed_for_distance = |dist: f32| -> f32 {
3768 if dist < DEAD_ZONE {
3769 0.0
3770 } else if dist < SLOW_ZONE {
3771 SLOW_SPEED
3772 } else if dist < MEDIUM_ZONE {
3773 MEDIUM_SPEED
3774 } else if dist < FAST_ZONE {
3775 FAST_SPEED
3776 } else {
3777 VERY_FAST_SPEED
3778 }
3779 };
3780
3781 let scroll_x = if distance.left < distance.right {
3783 -speed_for_distance(distance.left)
3785 } else {
3786 speed_for_distance(distance.right)
3788 };
3789
3790 let scroll_y = if distance.top < distance.bottom {
3792 -speed_for_distance(distance.top)
3794 } else {
3795 speed_for_distance(distance.bottom)
3797 };
3798
3799 LogicalPosition::new(scroll_x, scroll_y)
3800}
3801
3802pub struct LayoutResult {
3804 pub display_list: DisplayList,
3805 pub warnings: Vec<String>,
3806}
3807
3808impl LayoutResult {
3809 pub fn new(display_list: DisplayList, warnings: Vec<String>) -> Self {
3810 Self {
3811 display_list,
3812 warnings,
3813 }
3814 }
3815}
3816
3817impl LayoutWindow {
3818 #[cfg(feature = "std")]
3823 pub fn run_single_timer(
3827 &mut self,
3828 timer_id: usize,
3829 frame_start: Instant,
3830 current_window_handle: &RawWindowHandle,
3831 gl_context: &OptionGlContextPtr,
3832 system_style: std::sync::Arc<azul_css::system::SystemStyle>,
3833 system_callbacks: &ExternalSystemCallbacks,
3834 previous_window_state: &Option<FullWindowState>,
3835 current_window_state: &FullWindowState,
3836 renderer_resources: &RendererResources,
3837 ) -> (Vec<crate::callbacks::CallbackChange>, Update) {
3838 use crate::callbacks::{CallbackInfo, CallbackChange};
3839
3840 let mut update = Update::DoNothing;
3841 let mut all_changes = Vec::new();
3842 let mut should_terminate = TerminateTimer::Continue;
3843
3844 let current_scroll_states_nested = self.get_nested_scroll_states(DomId::ROOT_ID);
3845
3846 let timer_exists = self.timers.contains_key(&TimerId { id: timer_id });
3847 let timer_node_id = self
3848 .timers
3849 .get(&TimerId { id: timer_id })
3850 .and_then(|t| t.node_id.into_option());
3851
3852 if timer_exists {
3853 let hit_dom_node = match timer_node_id {
3854 Some(s) => s,
3855 None => DomNodeId {
3856 dom: DomId::ROOT_ID,
3857 node: NodeHierarchyItemId::from_crate_internal(None),
3858 },
3859 };
3860 let cursor_relative_to_item = OptionLogicalPosition::None;
3861 let cursor_in_viewport = OptionLogicalPosition::None;
3862
3863 let callback_changes = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
3864
3865 let timer_ctx = self
3866 .timers
3867 .get(&TimerId { id: timer_id })
3868 .map(|t| t.callback.ctx.clone())
3869 .unwrap_or(OptionRefAny::None);
3870
3871 let ref_data = crate::callbacks::CallbackInfoRefData {
3872 layout_window: self,
3873 renderer_resources,
3874 previous_window_state,
3875 current_window_state,
3876 gl_context,
3877 current_scroll_manager: ¤t_scroll_states_nested,
3878 current_window_handle,
3879 system_callbacks,
3880 system_style,
3881 monitors: self.monitors.clone(),
3882 #[cfg(feature = "icu")]
3883 icu_localizer: self.icu_localizer.clone(),
3884 ctx: timer_ctx,
3885 };
3886
3887 let callback_info = CallbackInfo::new(
3888 &ref_data,
3889 &callback_changes,
3890 hit_dom_node,
3891 cursor_relative_to_item,
3892 cursor_in_viewport,
3893 );
3894
3895 let timer = self.timers.get_mut(&TimerId { id: timer_id }).unwrap();
3896 let tcr = timer.invoke(&callback_info, &system_callbacks.get_system_time_fn);
3897
3898 update = tcr.should_update;
3899 should_terminate = tcr.should_terminate;
3900
3901 all_changes = callback_changes
3902 .lock()
3903 .map(|mut guard| core::mem::take(&mut *guard))
3904 .unwrap_or_default();
3905 }
3906
3907 if should_terminate == TerminateTimer::Terminate {
3908 all_changes.push(CallbackChange::RemoveTimer {
3909 timer_id: TimerId { id: timer_id },
3910 });
3911 }
3912
3913 (all_changes, update)
3914 }
3915
3916 #[cfg(feature = "std")]
3917 pub fn run_all_threads(
3919 &mut self,
3920 data: &mut RefAny,
3921 current_window_handle: &RawWindowHandle,
3922 gl_context: &OptionGlContextPtr,
3923 system_style: std::sync::Arc<azul_css::system::SystemStyle>,
3924 system_callbacks: &ExternalSystemCallbacks,
3925 previous_window_state: &Option<FullWindowState>,
3926 current_window_state: &FullWindowState,
3927 renderer_resources: &RendererResources,
3928 ) -> (Vec<crate::callbacks::CallbackChange>, Update) {
3929 use std::collections::BTreeSet;
3930
3931 use crate::{
3932 callbacks::{CallbackInfo, CallbackChange},
3933 thread::{OptionThreadReceiveMsg, ThreadReceiveMsg, ThreadWriteBackMsg},
3934 };
3935
3936 let mut update = Update::DoNothing;
3937 let mut all_changes = Vec::new();
3938
3939 let current_scroll_states = self.get_nested_scroll_states(DomId::ROOT_ID);
3940
3941 let thread_ids: Vec<ThreadId> = self.threads.keys().copied().collect();
3942
3943 for thread_id in thread_ids {
3944 let thread = match self.threads.get_mut(&thread_id) {
3945 Some(t) => t,
3946 None => continue,
3947 };
3948
3949 let hit_dom_node = DomNodeId {
3950 dom: DomId::ROOT_ID,
3951 node: NodeHierarchyItemId::from_crate_internal(None),
3952 };
3953 let cursor_relative_to_item = OptionLogicalPosition::None;
3954 let cursor_in_viewport = OptionLogicalPosition::None;
3955
3956 let (msg, writeback_data_ptr, is_finished) = {
3957 let thread_inner = &mut *match thread.ptr.lock().ok() {
3958 Some(s) => s,
3959 None => {
3960 all_changes.push(CallbackChange::RemoveThread { thread_id });
3961 continue;
3962 }
3963 };
3964
3965 let _ = thread_inner.sender_send(ThreadSendMsg::Tick);
3966 let recv = thread_inner.receiver_try_recv();
3967 let msg = match recv {
3968 OptionThreadReceiveMsg::None => continue,
3969 OptionThreadReceiveMsg::Some(s) => s,
3970 };
3971
3972 let writeback_data_ptr: *mut RefAny = &mut thread_inner.writeback_data as *mut _;
3973 let is_finished = thread_inner.is_finished();
3974
3975 (msg, writeback_data_ptr, is_finished)
3976 };
3977
3978 let ThreadWriteBackMsg {
3979 refany: mut data_inner,
3980 callback,
3981 } = match msg {
3982 ThreadReceiveMsg::Update(update_screen) => {
3983 update.max_self(update_screen);
3984 continue;
3985 }
3986 ThreadReceiveMsg::WriteBack(t) => t,
3987 };
3988
3989 let callback_changes = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
3990
3991 let ref_data = crate::callbacks::CallbackInfoRefData {
3992 layout_window: self,
3993 renderer_resources,
3994 previous_window_state,
3995 current_window_state,
3996 gl_context,
3997 current_scroll_manager: ¤t_scroll_states,
3998 current_window_handle,
3999 system_callbacks,
4000 system_style: system_style.clone(),
4001 monitors: self.monitors.clone(),
4002 #[cfg(feature = "icu")]
4003 icu_localizer: self.icu_localizer.clone(),
4004 ctx: callback.ctx.clone(),
4005 };
4006
4007 let callback_info = CallbackInfo::new(
4008 &ref_data,
4009 &callback_changes,
4010 hit_dom_node,
4011 cursor_relative_to_item,
4012 cursor_in_viewport,
4013 );
4014
4015 let callback_update = (callback.cb)(
4016 unsafe { (*writeback_data_ptr).clone() },
4017 data_inner.clone(),
4018 callback_info,
4019 );
4020 update.max_self(callback_update);
4021
4022 let collected_changes = callback_changes
4023 .lock()
4024 .map(|mut guard| core::mem::take(&mut *guard))
4025 .unwrap_or_default();
4026
4027 all_changes.extend(collected_changes);
4028
4029 if is_finished {
4030 all_changes.push(CallbackChange::RemoveThread { thread_id });
4031 }
4032 }
4033
4034 (all_changes, update)
4035 }
4036
4037 pub fn invoke_single_callback(
4042 &mut self,
4043 callback: &mut Callback,
4044 data: &mut RefAny,
4045 current_window_handle: &RawWindowHandle,
4046 gl_context: &OptionGlContextPtr,
4047 system_style: std::sync::Arc<azul_css::system::SystemStyle>,
4048 system_callbacks: &ExternalSystemCallbacks,
4049 previous_window_state: &Option<FullWindowState>,
4050 current_window_state: &FullWindowState,
4051 renderer_resources: &RendererResources,
4052 ) -> (Vec<crate::callbacks::CallbackChange>, Update) {
4053 use crate::callbacks::{CallbackInfo, CallbackChange};
4054
4055 let hit_dom_node = DomNodeId {
4056 dom: DomId::ROOT_ID,
4057 node: NodeHierarchyItemId::from_crate_internal(None),
4058 };
4059
4060 let current_scroll_states = self.get_nested_scroll_states(DomId::ROOT_ID);
4061
4062 let cursor_relative_to_item = OptionLogicalPosition::None;
4063 let cursor_in_viewport = match current_window_state.mouse_state.cursor_position.get_position() {
4064 Some(pos) => OptionLogicalPosition::Some(pos),
4065 None => OptionLogicalPosition::None,
4066 };
4067
4068 let callback_changes = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
4070
4071 let ref_data = crate::callbacks::CallbackInfoRefData {
4079 layout_window: self,
4080 renderer_resources,
4081 previous_window_state,
4082 current_window_state,
4083 gl_context,
4084 current_scroll_manager: ¤t_scroll_states,
4085 current_window_handle,
4086 system_callbacks,
4087 system_style,
4088 monitors: self.monitors.clone(),
4089 #[cfg(feature = "icu")]
4090 icu_localizer: self.icu_localizer.clone(),
4091 ctx: callback.ctx.clone(),
4092 };
4093
4094 let callback_info = CallbackInfo::new(
4095 &ref_data,
4096 &callback_changes,
4097 hit_dom_node,
4098 cursor_relative_to_item,
4099 cursor_in_viewport,
4100 );
4101
4102 let update = (callback.cb)(data.clone(), callback_info);
4103
4104 let collected_changes = callback_changes
4106 .lock()
4107 .map(|mut guard| core::mem::take(&mut *guard))
4108 .unwrap_or_default();
4109
4110 (collected_changes, update)
4111 }
4112
4113 pub fn set_system_style(&mut self, system_style: std::sync::Arc<azul_css::system::SystemStyle>) {
4121 #[cfg(feature = "icu")]
4122 {
4123 self.icu_localizer = crate::icu::IcuLocalizerHandle::from_system_language(&system_style.language);
4124 }
4125 self.system_style = Some(system_style);
4126 }
4127}
4128
4129#[cfg(feature = "icu")]
4132impl LayoutWindow {
4133 pub fn set_icu_locale(&mut self, locale: &str) {
4141 self.icu_localizer.set_locale(locale);
4142 }
4143
4144 pub fn init_icu_from_system_style(&mut self, system_style: &azul_css::system::SystemStyle) {
4148 self.icu_localizer = IcuLocalizerHandle::from_system_language(&system_style.language);
4149 }
4150
4151 pub fn get_icu_localizer(&self) -> IcuLocalizerHandle {
4155 self.icu_localizer.clone()
4156 }
4157
4158 pub fn load_icu_data_blob(&mut self, data: Vec<u8>) -> bool {
4163 self.icu_localizer.load_data_blob(&data)
4164 }
4165}
4166
4167#[cfg(test)]
4168mod tests {
4169 use super::*;
4170 use crate::{thread::Thread, timer::Timer};
4171
4172 #[test]
4173 fn test_timer_add_remove() {
4174 let fc_cache = FcFontCache::default();
4175 let mut window = LayoutWindow::new(fc_cache).unwrap();
4176
4177 let timer_id = TimerId { id: 1 };
4178 let timer = Timer::default();
4179
4180 window.add_timer(timer_id, timer);
4182 assert!(window.get_timer(&timer_id).is_some());
4183 assert_eq!(window.get_timer_ids().len(), 1);
4184
4185 let removed = window.remove_timer(&timer_id);
4187 assert!(removed.is_some());
4188 assert!(window.get_timer(&timer_id).is_none());
4189 assert_eq!(window.get_timer_ids().len(), 0);
4190 }
4191
4192 #[test]
4193 fn test_timer_get_mut() {
4194 let fc_cache = FcFontCache::default();
4195 let mut window = LayoutWindow::new(fc_cache).unwrap();
4196
4197 let timer_id = TimerId { id: 1 };
4198 let timer = Timer::default();
4199
4200 window.add_timer(timer_id, timer);
4201
4202 let timer_mut = window.get_timer_mut(&timer_id);
4204 assert!(timer_mut.is_some());
4205 }
4206
4207 #[test]
4208 fn test_multiple_timers() {
4209 let fc_cache = FcFontCache::default();
4210 let mut window = LayoutWindow::new(fc_cache).unwrap();
4211
4212 let timer1 = TimerId { id: 1 };
4213 let timer2 = TimerId { id: 2 };
4214 let timer3 = TimerId { id: 3 };
4215
4216 window.add_timer(timer1, Timer::default());
4217 window.add_timer(timer2, Timer::default());
4218 window.add_timer(timer3, Timer::default());
4219
4220 assert_eq!(window.get_timer_ids().len(), 3);
4221
4222 window.remove_timer(&timer2);
4223 assert_eq!(window.get_timer_ids().len(), 2);
4224 assert!(window.get_timer(&timer1).is_some());
4225 assert!(window.get_timer(&timer2).is_none());
4226 assert!(window.get_timer(&timer3).is_some());
4227 }
4228
4229 #[test]
4234 fn test_gpu_cache_management() {
4235 let fc_cache = FcFontCache::default();
4236 let mut window = LayoutWindow::new(fc_cache).unwrap();
4237
4238 let dom_id = DomId { inner: 0 };
4239
4240 assert!(window.get_gpu_cache(&dom_id).is_none());
4242
4243 let cache = window.get_or_create_gpu_cache(dom_id);
4245 assert!(cache.transform_keys.is_empty());
4246
4247 assert!(window.get_gpu_cache(&dom_id).is_some());
4249
4250 let cache_mut = window.get_gpu_cache_mut(&dom_id);
4252 assert!(cache_mut.is_some());
4253 }
4254
4255 #[test]
4256 fn test_gpu_cache_multiple_doms() {
4257 let fc_cache = FcFontCache::default();
4258 let mut window = LayoutWindow::new(fc_cache).unwrap();
4259
4260 let dom1 = DomId { inner: 0 };
4261 let dom2 = DomId { inner: 1 };
4262
4263 window.get_or_create_gpu_cache(dom1);
4264 window.get_or_create_gpu_cache(dom2);
4265
4266 assert!(window.get_gpu_cache(&dom1).is_some());
4267 assert!(window.get_gpu_cache(&dom2).is_some());
4268 }
4269
4270 #[test]
4271 fn test_compute_cursor_type_empty_hit_test() {
4272 use crate::hit_test::FullHitTest;
4273
4274 let fc_cache = FcFontCache::default();
4275 let window = LayoutWindow::new(fc_cache).unwrap();
4276
4277 let empty_hit = FullHitTest::empty(None);
4278 let cursor_test = window.compute_cursor_type_hit_test(&empty_hit);
4279
4280 assert_eq!(
4282 cursor_test.cursor_icon,
4283 azul_core::window::MouseCursorType::Default
4284 );
4285 assert!(cursor_test.cursor_node.is_none());
4286 }
4287
4288 #[test]
4289 fn test_layout_result_access() {
4290 let fc_cache = FcFontCache::default();
4291 let window = LayoutWindow::new(fc_cache).unwrap();
4292
4293 let dom_id = DomId { inner: 0 };
4294
4295 assert!(window.get_layout_result(&dom_id).is_none());
4297 assert_eq!(window.get_dom_ids().len(), 0);
4298 }
4299
4300 #[test]
4303 fn test_scroll_manager_initialization() {
4304 let fc_cache = FcFontCache::default();
4305 let window = LayoutWindow::new(fc_cache).unwrap();
4306
4307 let dom_id = DomId::ROOT_ID;
4308 let node_id = NodeId::new(0);
4309
4310 let scroll_offsets = window.scroll_manager.get_scroll_states_for_dom(dom_id);
4312 assert!(scroll_offsets.is_empty());
4313
4314 let offset = window.scroll_manager.get_current_offset(dom_id, node_id);
4316 assert_eq!(offset, None);
4317 }
4318
4319 #[test]
4320 fn test_scroll_manager_tick_updates_activity() {
4321 let fc_cache = FcFontCache::default();
4322 let mut window = LayoutWindow::new(fc_cache).unwrap();
4323
4324 let dom_id = DomId::ROOT_ID;
4325 let node_id = NodeId::new(0);
4326
4327 #[cfg(feature = "std")]
4329 let now = Instant::System(std::time::Instant::now().into());
4330 #[cfg(not(feature = "std"))]
4331 let now = Instant::Tick(azul_core::task::SystemTick { tick_counter: 0 });
4332
4333 let scroll_input = crate::managers::scroll_state::ScrollInput {
4334 dom_id,
4335 node_id,
4336 delta: LogicalPosition::new(10.0, 20.0),
4337 timestamp: now.clone(),
4338 source: crate::managers::scroll_state::ScrollInputSource::WheelDiscrete,
4339 };
4340
4341 let should_start_timer = window
4342 .scroll_manager
4343 .record_scroll_input(scroll_input);
4344
4345 assert!(should_start_timer);
4347 }
4348
4349 #[test]
4350 fn test_scroll_manager_programmatic_scroll() {
4351 let fc_cache = FcFontCache::default();
4352 let mut window = LayoutWindow::new(fc_cache).unwrap();
4353
4354 let dom_id = DomId::ROOT_ID;
4355 let node_id = NodeId::new(0);
4356
4357 #[cfg(feature = "std")]
4358 let now = Instant::System(std::time::Instant::now().into());
4359 #[cfg(not(feature = "std"))]
4360 let now = Instant::Tick(azul_core::task::SystemTick { tick_counter: 0 });
4361
4362 window.scroll_manager.scroll_to(
4364 dom_id,
4365 node_id,
4366 LogicalPosition::new(100.0, 200.0),
4367 Duration::System(SystemTimeDiff::from_millis(300)),
4368 EasingFunction::EaseOut,
4369 now.clone(),
4370 );
4371
4372 let tick_result = window.scroll_manager.tick(now);
4373
4374 assert!(tick_result.needs_repaint);
4376 }
4377
4378
4379
4380 #[test]
4381 fn test_gpu_cache_scrollbar_opacity_keys() {
4382 let fc_cache = FcFontCache::default();
4383 let mut window = LayoutWindow::new(fc_cache).unwrap();
4384
4385 let dom_id = DomId::ROOT_ID;
4386 let node_id = NodeId::new(0);
4387
4388 let gpu_cache = window.get_or_create_gpu_cache(dom_id);
4390
4391 assert!(gpu_cache.scrollbar_v_opacity_keys.is_empty());
4393 assert!(gpu_cache.scrollbar_h_opacity_keys.is_empty());
4394
4395 let opacity_key = azul_core::resources::OpacityKey::unique();
4397 gpu_cache
4398 .scrollbar_v_opacity_keys
4399 .insert((dom_id, node_id), opacity_key);
4400 gpu_cache
4401 .scrollbar_v_opacity_values
4402 .insert((dom_id, node_id), 1.0);
4403
4404 assert_eq!(gpu_cache.scrollbar_v_opacity_keys.len(), 1);
4406 assert_eq!(
4407 gpu_cache.scrollbar_v_opacity_values.get(&(dom_id, node_id)),
4408 Some(&1.0)
4409 );
4410 }
4411
4412
4413}
4414
4415impl LayoutWindow {
4417 pub fn find_next_text_node(
4430 &self,
4431 dom_id: &DomId,
4432 current_node: NodeId,
4433 ) -> Option<(DomId, NodeId)> {
4434 let layout_result = self.get_layout_result(dom_id)?;
4435 let styled_dom = &layout_result.styled_dom;
4436
4437 let start_idx = current_node.index() + 1;
4439 let node_hierarchy = &styled_dom.node_hierarchy;
4440
4441 for i in start_idx..node_hierarchy.len() {
4442 let node_id = NodeId::new(i);
4443
4444 if self.node_has_text_content(styled_dom, node_id) {
4446 if self.is_text_selectable(styled_dom, node_id) {
4448 return Some((*dom_id, node_id));
4449 }
4450 }
4451 }
4452
4453 None
4454 }
4455
4456 pub fn find_prev_text_node(
4469 &self,
4470 dom_id: &DomId,
4471 current_node: NodeId,
4472 ) -> Option<(DomId, NodeId)> {
4473 let layout_result = self.get_layout_result(dom_id)?;
4474 let styled_dom = &layout_result.styled_dom;
4475
4476 let current_idx = current_node.index();
4478
4479 for i in (0..current_idx).rev() {
4480 let node_id = NodeId::new(i);
4481
4482 if self.node_has_text_content(styled_dom, node_id) {
4484 if self.is_text_selectable(styled_dom, node_id) {
4486 return Some((*dom_id, node_id));
4487 }
4488 }
4489 }
4490
4491 None
4492 }
4493
4494 fn find_last_text_child(&self, dom_id: DomId, parent_node_id: NodeId) -> Option<NodeId> {
4500 let layout_result = self.layout_results.get(&dom_id)?;
4501 let styled_dom = &layout_result.styled_dom;
4502 let node_data_container = styled_dom.node_data.as_container();
4503 let hierarchy_container = styled_dom.node_hierarchy.as_container();
4504
4505 let parent_type = node_data_container[parent_node_id].get_node_type();
4507 if matches!(parent_type, NodeType::Text(_)) {
4508 return Some(parent_node_id);
4509 }
4510
4511 let parent_item = &hierarchy_container[parent_node_id];
4513 let mut last_text_child: Option<NodeId> = None;
4514 let mut current_child = parent_item.first_child_id(parent_node_id);
4515 while let Some(child_id) = current_child {
4516 let child_type = node_data_container[child_id].get_node_type();
4517 if matches!(child_type, NodeType::Text(_)) {
4518 last_text_child = Some(child_id);
4519 }
4520 current_child = hierarchy_container[child_id].next_sibling_id();
4521 }
4522
4523 last_text_child
4524 }
4525
4526 fn node_has_text_content(&self, styled_dom: &StyledDom, node_id: NodeId) -> bool {
4528 let node_data_container = styled_dom.node_data.as_container();
4530 let node_type = node_data_container[node_id].get_node_type();
4531 if matches!(node_type, NodeType::Text(_)) {
4532 return true;
4533 }
4534
4535 let hierarchy_container = styled_dom.node_hierarchy.as_container();
4537 let node_item = &hierarchy_container[node_id];
4538
4539 let mut current_child = node_item.first_child_id(node_id);
4541 while let Some(child_id) = current_child {
4542 let child_type = node_data_container[child_id].get_node_type();
4543 if matches!(child_type, NodeType::Text(_)) {
4544 return true;
4545 }
4546
4547 current_child = hierarchy_container[child_id].next_sibling_id();
4549 }
4550
4551 false
4552 }
4553
4554 fn is_text_selectable(&self, styled_dom: &StyledDom, node_id: NodeId) -> bool {
4556 let node_state = &styled_dom.styled_nodes.as_container()[node_id].styled_node_state;
4557 crate::solver3::getters::is_text_selectable(styled_dom, node_id, node_state)
4558 }
4559
4560 #[cfg(feature = "a11y")]
4579 pub fn process_accessibility_action(
4580 &mut self,
4581 dom_id: DomId,
4582 node_id: NodeId,
4583 action: azul_core::dom::AccessibilityAction,
4584 now: std::time::Instant,
4585 ) -> BTreeMap<DomNodeId, (Vec<azul_core::events::EventFilter>, bool)> {
4586 use crate::managers::text_input::TextInputSource;
4587
4588 let mut affected_nodes = BTreeMap::new();
4589
4590 match action {
4591 AccessibilityAction::Focus => {
4593 let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(node_id));
4594 let dom_node_id = DomNodeId {
4595 dom: dom_id,
4596 node: hierarchy_id,
4597 };
4598 self.focus_manager.set_focused_node(Some(dom_node_id));
4599
4600 if let Some(layout_result) = self.layout_results.get(&dom_id) {
4602 if let Some(styled_node) = layout_result
4603 .styled_dom
4604 .node_data
4605 .as_ref()
4606 .get(node_id.index())
4607 {
4608 let is_contenteditable = styled_node.is_contenteditable()
4612 || styled_node.attributes().as_ref().iter().any(|attr| {
4613 matches!(attr, azul_core::dom::AttributeType::ContentEditable(_))
4614 });
4615
4616 if is_contenteditable {
4617 let inline_layout = self.get_inline_layout_for_node(dom_id, node_id).cloned();
4620 if let Some(ref layout) = inline_layout {
4621 let cursor = layout.items.iter().rev()
4622 .find_map(|item| if let crate::text3::cache::ShapedItem::Cluster(c) = &item.item {
4623 Some(azul_core::selection::TextCursor {
4624 cluster_id: c.source_cluster_id,
4625 affinity: azul_core::selection::CursorAffinity::Trailing,
4626 })
4627 } else { None })
4628 .unwrap_or(azul_core::selection::TextCursor {
4629 cluster_id: azul_core::selection::GraphemeClusterId { source_run: 0, start_byte_in_run: 0 },
4630 affinity: azul_core::selection::CursorAffinity::Trailing,
4631 });
4632 self.text_edit_manager.initialize_editing(cursor, dom_id, node_id, 0);
4633
4634 self.scroll_cursor_into_view_if_needed(dom_id, node_id, now);
4636 }
4637 } else {
4638 self.text_edit_manager.clear_editing();
4640 }
4641 }
4642 }
4643
4644 self.scroll_to_node_if_needed(dom_id, node_id, now);
4646 }
4647 AccessibilityAction::Blur => {
4648 self.focus_manager.clear_focus();
4649 self.text_edit_manager.clear_editing();
4650 }
4651 AccessibilityAction::SetSequentialFocusNavigationStartingPoint => {
4652 let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(node_id));
4653 let dom_node_id = DomNodeId {
4654 dom: dom_id,
4655 node: hierarchy_id,
4656 };
4657 self.focus_manager.set_focused_node(Some(dom_node_id));
4658 self.text_edit_manager.clear_editing();
4660 }
4661
4662 AccessibilityAction::ScrollIntoView => {
4664 self.scroll_to_node_if_needed(dom_id, node_id, now);
4665 }
4666 AccessibilityAction::ScrollLeft |
4667 AccessibilityAction::ScrollRight |
4668 AccessibilityAction::ScrollUp |
4669 AccessibilityAction::ScrollDown => {
4670 let dom_node_id = DomNodeId {
4672 dom: dom_id,
4673 node: NodeHierarchyItemId::from_crate_internal(Some(node_id)),
4674 };
4675 let (scroll_dom, scroll_nid) = self.find_scrollable_ancestor(dom_node_id)
4676 .and_then(|a| Some((a.dom, a.node.into_crate_internal()?)))
4677 .unwrap_or((dom_id, node_id));
4678
4679 let bounds = self.get_node_bounds(scroll_dom, scroll_nid);
4681 let vp_h = bounds.map(|b| b.size.height as f32).unwrap_or(600.0);
4682 let vp_w = bounds.map(|b| b.size.width as f32).unwrap_or(800.0);
4683
4684 let (dx, dy) = match action {
4685 AccessibilityAction::ScrollLeft => (-vp_w * 0.75, 0.0),
4686 AccessibilityAction::ScrollRight => ( vp_w * 0.75, 0.0),
4687 AccessibilityAction::ScrollUp => (0.0, -vp_h * 0.75),
4688 AccessibilityAction::ScrollDown => (0.0, vp_h * 0.75),
4689 _ => unreachable!(),
4690 };
4691
4692 self.scroll_manager.scroll_by(
4693 scroll_dom,
4694 scroll_nid,
4695 LogicalPosition { x: dx, y: dy },
4696 std::time::Duration::from_millis(250).into(),
4697 azul_core::events::EasingFunction::EaseOut,
4698 now.into(),
4699 );
4700 }
4701 AccessibilityAction::SetScrollOffset(pos) => {
4702 self.scroll_manager.scroll_to(
4703 dom_id,
4704 node_id,
4705 pos,
4706 std::time::Duration::from_millis(0).into(),
4707 azul_core::events::EasingFunction::Linear,
4708 now.into(),
4709 );
4710 }
4711 AccessibilityAction::ScrollToPoint(pos) => {
4712 self.scroll_manager.scroll_to(
4713 dom_id,
4714 node_id,
4715 pos,
4716 std::time::Duration::from_millis(300).into(),
4717 azul_core::events::EasingFunction::EaseInOut,
4718 now.into(),
4719 );
4720 }
4721
4722 AccessibilityAction::Default => {
4726 let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(node_id));
4728 let dom_node_id = DomNodeId {
4729 dom: dom_id,
4730 node: hierarchy_id,
4731 };
4732
4733 let event_filter = EventFilter::Hover(HoverEventFilter::MouseUp);
4735
4736 affected_nodes.insert(dom_node_id, (vec![event_filter], false));
4737 }
4738
4739 AccessibilityAction::Increment | AccessibilityAction::Decrement => {
4740 let is_increment = matches!(action, AccessibilityAction::Increment);
4750
4751 let current_value = if let Some(layout_result) = self.layout_results.get(&dom_id) {
4753 if let Some(styled_node) = layout_result
4754 .styled_dom
4755 .node_data
4756 .as_ref()
4757 .get(node_id.index())
4758 {
4759 styled_node
4761 .attributes()
4762 .as_ref()
4763 .iter()
4764 .find_map(|attr| {
4765 if let AttributeType::Value(v) = attr {
4766 Some(v.as_str().to_string())
4767 } else {
4768 None
4769 }
4770 })
4771 .or_else(|| {
4772 if let NodeType::Text(text) = styled_node.get_node_type() {
4774 Some(text.as_str().to_string())
4775 } else {
4776 None
4777 }
4778 })
4779 } else {
4780 None
4781 }
4782 } else {
4783 None
4784 };
4785
4786 if let Some(value_str) = current_value {
4788 let parsed: Result<f64, _> = value_str.trim().parse();
4789
4790 let new_value_str = if let Ok(num) = parsed {
4791 let new_num = if is_increment { num + 1.0 } else { num - 1.0 };
4793 if num.fract() == 0.0 {
4795 format!("{}", new_num as i64)
4796 } else {
4797 format!("{}", new_num)
4798 }
4799 } else {
4800 if is_increment {
4802 "1".to_string()
4803 } else {
4804 "-1".to_string()
4805 }
4806 };
4807
4808 let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(node_id));
4810 let dom_node_id = DomNodeId {
4811 dom: dom_id,
4812 node: hierarchy_id,
4813 };
4814
4815 let old_inline_content = self.get_text_before_textinput(dom_id, node_id);
4817 let old_text = self.extract_text_from_inline_content(&old_inline_content);
4818
4819 self.text_input_manager.record_input(
4821 dom_node_id,
4822 new_value_str,
4823 old_text,
4824 TextInputSource::Accessibility,
4825 );
4826
4827 affected_nodes.insert(
4829 dom_node_id,
4830 (vec![EventFilter::Focus(FocusEventFilter::TextInput)], false),
4831 );
4832 }
4833 }
4834
4835 AccessibilityAction::Collapse | AccessibilityAction::Expand => {
4836 let event_type = match action {
4838 AccessibilityAction::Collapse => On::Collapse,
4839 AccessibilityAction::Expand => On::Expand,
4840 _ => unreachable!(),
4841 };
4842
4843 if let Some(layout_result) = self.layout_results.get(&dom_id) {
4845 if let Some(styled_node) = layout_result
4846 .styled_dom
4847 .node_data
4848 .as_ref()
4849 .get(node_id.index())
4850 {
4851 let has_callback = styled_node
4853 .callbacks
4854 .as_ref()
4855 .iter()
4856 .any(|cb| cb.event == event_type.into());
4857
4858 let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(node_id));
4859 let dom_node_id = DomNodeId {
4860 dom: dom_id,
4861 node: hierarchy_id,
4862 };
4863
4864 if has_callback {
4865 affected_nodes.insert(dom_node_id, (vec![event_type.into()], false));
4867 } else {
4868 affected_nodes.insert(
4870 dom_node_id,
4871 (vec![EventFilter::Hover(HoverEventFilter::MouseUp)], false),
4872 );
4873 }
4874 }
4875 }
4876 }
4877
4878 AccessibilityAction::ShowContextMenu => {
4880 let layout_result = match self.layout_results.get(&dom_id) {
4882 Some(lr) => lr,
4883 None => {
4884 return affected_nodes;
4885 }
4886 };
4887
4888 let styled_node = match layout_result
4890 .styled_dom
4891 .node_data
4892 .as_ref()
4893 .get(node_id.index())
4894 {
4895 Some(node) => node,
4896 None => {
4897 return affected_nodes;
4898 }
4899 };
4900
4901 let has_context_menu = styled_node.get_context_menu().is_some();
4903
4904 if has_context_menu {
4905 let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(node_id));
4908 let dom_node_id = DomNodeId { dom: dom_id, node: hierarchy_id };
4909 affected_nodes.insert(
4910 dom_node_id,
4911 (vec![azul_core::events::EventFilter::Hover(
4912 azul_core::events::HoverEventFilter::RightMouseDown,
4913 )], false),
4914 );
4915 }
4916 }
4917
4918 AccessibilityAction::ReplaceSelectedText(ref text) => {
4920 let nodes = self.edit_text_node(
4921 dom_id,
4922 node_id,
4923 TextEditType::ReplaceSelection(text.as_str().to_string()),
4924 );
4925 for node in nodes {
4926 affected_nodes.insert(node, (Vec::new(), true)); }
4928 }
4929 AccessibilityAction::SetValue(ref text) => {
4930 let nodes = self.edit_text_node(
4931 dom_id,
4932 node_id,
4933 TextEditType::SetValue(text.as_str().to_string()),
4934 );
4935 for node in nodes {
4936 affected_nodes.insert(node, (Vec::new(), true));
4937 }
4938 }
4939 AccessibilityAction::SetNumericValue(value) => {
4940 let nodes = self.edit_text_node(
4941 dom_id,
4942 node_id,
4943 TextEditType::SetNumericValue(value.get() as f64),
4944 );
4945 for node in nodes {
4946 affected_nodes.insert(node, (Vec::new(), true));
4947 }
4948 }
4949 AccessibilityAction::SetTextSelection(selection) => {
4950 let text_layout = self.get_node_inline_layout(dom_id, node_id);
4952
4953 if let Some(inline_layout) = text_layout {
4954 let start_cursor = self.byte_offset_to_cursor(
4956 inline_layout.as_ref(),
4957 selection.selection_start as u32,
4958 );
4959 let end_cursor = self.byte_offset_to_cursor(
4960 inline_layout.as_ref(),
4961 selection.selection_end as u32,
4962 );
4963
4964 if let (Some(start), Some(end)) = (start_cursor, end_cursor) {
4965 let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(node_id));
4966 let dom_node_id = DomNodeId {
4967 dom: dom_id,
4968 node: hierarchy_id,
4969 };
4970
4971 if start == end {
4972 if let Some(ref mut mc) = self.text_edit_manager.multi_cursor {
4974 mc.set_single_cursor(start);
4975 }
4976 } else {
4977 if let Some(ref mut mc) = self.text_edit_manager.multi_cursor {
4979 mc.set_single_cursor(start);
4980 }
4981 }
4982 } else {
4983 }
4985 } else {
4986 }
4988 }
4989
4990 AccessibilityAction::ShowTooltip | AccessibilityAction::HideTooltip => {
4992 }
4994
4995 AccessibilityAction::CustomAction(_id) => {
4996 }
4998 }
4999
5000 affected_nodes
5001 }
5002
5003 pub fn record_text_input(
5025 &mut self,
5026 text_input: &str,
5027 ) -> BTreeMap<azul_core::dom::DomNodeId, (Vec<azul_core::events::EventFilter>, bool)> {
5028 use std::collections::BTreeMap;
5029
5030 use crate::managers::text_input::TextInputSource;
5031
5032 let mut affected_nodes = BTreeMap::new();
5033
5034 if text_input.is_empty() {
5035 return affected_nodes;
5036 }
5037
5038 let focused_node = match self.focus_manager.get_focused_node().copied() {
5040 Some(node) => node,
5041 None => {
5042 return affected_nodes;
5043 }
5044 };
5045
5046 let node_id = match focused_node.node.into_crate_internal() {
5047 Some(id) => id,
5048 None => {
5049 return affected_nodes;
5050 }
5051 };
5052
5053 let old_inline_content = self.get_text_before_textinput(focused_node.dom, node_id);
5055 let old_text = self.extract_text_from_inline_content(&old_inline_content);
5056
5057 self.text_input_manager.record_input(
5059 focused_node,
5060 text_input.to_string(),
5061 old_text,
5062 TextInputSource::Keyboard, );
5064
5065 let text_input_event = vec![EventFilter::Focus(FocusEventFilter::TextInput)];
5067
5068 affected_nodes.insert(focused_node, (text_input_event, false)); affected_nodes
5071 }
5072
5073 pub fn apply_text_changeset(&mut self) -> TextChangesetResult {
5083 let empty = TextChangesetResult { dirty_nodes: Vec::new(), needs_relayout: false };
5085
5086 let changeset = match self.text_input_manager.get_pending_changeset() {
5087 Some(cs) => {
5088 cs.clone()
5089 }
5090 None => {
5091 return empty;
5092 }
5093 };
5094
5095 let node_id = match changeset.node.node.into_crate_internal() {
5096 Some(id) => id,
5097 None => {
5098 self.text_input_manager.clear_changeset();
5099 return empty;
5100 }
5101 };
5102
5103 let dom_id = changeset.node.dom;
5104
5105 let layout_result = match self.layout_results.get(&dom_id) {
5107 Some(lr) => lr,
5108 None => {
5109 self.text_input_manager.clear_changeset();
5110 return empty;
5111 }
5112 };
5113
5114 let styled_node = match layout_result
5115 .styled_dom
5116 .node_data
5117 .as_ref()
5118 .get(node_id.index())
5119 {
5120 Some(node) => node,
5121 None => {
5122 self.text_input_manager.clear_changeset();
5123 return empty;
5124 }
5125 };
5126
5127 let is_contenteditable = styled_node.is_contenteditable()
5131 || styled_node.attributes().as_ref().iter().any(|attr| {
5132 matches!(attr, azul_core::dom::AttributeType::ContentEditable(_))
5133 });
5134
5135 if !is_contenteditable {
5136 self.text_input_manager.clear_changeset();
5137 return empty;
5138 }
5139
5140 let content = self.get_text_before_textinput(dom_id, node_id);
5142
5143 let mc_selections = self.text_edit_manager.multi_cursor.as_ref()
5145 .map(|mc| mc.to_selections())
5146 .unwrap_or_default();
5147 let current_selection = if !mc_selections.is_empty() {
5148 mc_selections
5149 } else if let Some(cursor) = self.text_edit_manager.get_primary_cursor() {
5150 vec![Selection::Cursor(cursor)]
5151 } else {
5152 vec![Selection::Cursor(TextCursor {
5153 cluster_id: GraphemeClusterId {
5154 source_run: 0,
5155 start_byte_in_run: 0,
5156 },
5157 affinity: CursorAffinity::Leading,
5158 })]
5159 };
5160
5161 let old_text = self.extract_text_from_inline_content(&content);
5163 let old_cursor = current_selection.first().and_then(|sel| {
5164 if let Selection::Cursor(c) = sel {
5165 Some(c.clone())
5166 } else {
5167 None
5168 }
5169 });
5170 let old_selection_range = current_selection.first().and_then(|sel| {
5171 if let Selection::Range(r) = sel {
5172 Some(*r)
5173 } else {
5174 None
5175 }
5176 });
5177
5178 let pre_state = crate::managers::undo_redo::NodeStateSnapshot {
5179 node_id: azul_core::id::NodeId::new(node_id.index()),
5180 text_content: old_text.into(),
5181 cursor_position: old_cursor.into(),
5182 selection_range: old_selection_range.into(),
5183 #[cfg(feature = "std")]
5184 timestamp: azul_core::task::Instant::System(std::time::Instant::now().into()),
5185 #[cfg(not(feature = "std"))]
5186 timestamp: azul_core::task::Instant::Tick(azul_core::task::SystemTick { tick_counter: 0 }),
5187 };
5188
5189 use crate::text3::edit::{edit_text, TextEdit};
5191 let text_edit = TextEdit::Insert(changeset.inserted_text.as_str().to_string());
5192 let (new_content, new_selections) = edit_text(&content, ¤t_selection, &text_edit);
5193
5194 if let Some(ref mut mc) = self.text_edit_manager.multi_cursor {
5196 mc.update_from_edit_result(&new_selections);
5197 }
5198 self.update_text_cache_after_edit(dom_id, node_id, new_content);
5202
5203 use crate::managers::changeset::{TextChangeset, TextOpInsertText, TextOperation};
5206
5207 let new_cursor = self
5209 .get_focused_cursor_rect()
5210 .map(|r| CursorPosition::InWindow(r.origin))
5211 .unwrap_or(CursorPosition::Uninitialized);
5212
5213 let old_cursor_pos = old_cursor
5214 .as_ref()
5215 .map(|_| {
5216 self.get_focused_cursor_rect()
5221 .map(|r| CursorPosition::InWindow(r.origin))
5222 .unwrap_or(CursorPosition::Uninitialized)
5223 })
5224 .unwrap_or(CursorPosition::Uninitialized);
5225
5226 static CHANGESET_COUNTER: std::sync::atomic::AtomicUsize =
5228 std::sync::atomic::AtomicUsize::new(0);
5229 let changeset_id = CHANGESET_COUNTER.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
5230
5231 let undo_changeset = TextChangeset {
5232 id: changeset_id,
5233 target: changeset.node,
5234 operation: TextOperation::InsertText(TextOpInsertText {
5235 text: changeset.inserted_text.clone(),
5236 position: old_cursor_pos,
5237 new_cursor,
5238 }),
5239 #[cfg(feature = "std")]
5240 timestamp: azul_core::task::Instant::System(std::time::Instant::now().into()),
5241 #[cfg(not(feature = "std"))]
5242 timestamp: azul_core::task::Instant::Tick(azul_core::task::SystemTick { tick_counter: 0 }),
5243 };
5244 self.undo_redo_manager
5245 .record_operation(undo_changeset, pre_state);
5246
5247 self.text_input_manager.clear_changeset();
5249
5250 let needs_relayout = self.dirty_text_nodes.values()
5252 .any(|d| d.needs_ancestor_relayout);
5253
5254 let dirty_nodes = self.determine_dirty_text_nodes(dom_id, node_id);
5256 TextChangesetResult { dirty_nodes, needs_relayout }
5257 }
5258
5259 fn determine_dirty_text_nodes(
5263 &self,
5264 dom_id: DomId,
5265 node_id: NodeId,
5266 ) -> Vec<azul_core::dom::DomNodeId> {
5267 let layout_result = match self.layout_results.get(&dom_id) {
5268 Some(lr) => lr,
5269 None => return Vec::new(),
5270 };
5271
5272 let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(node_id));
5273 let node_dom_id = azul_core::dom::DomNodeId {
5274 dom: dom_id,
5275 node: hierarchy_id,
5276 };
5277
5278 let parent_id = layout_result
5280 .styled_dom
5281 .node_hierarchy
5282 .as_container()
5283 .get(node_id)
5284 .and_then(|item| item.parent_id())
5285 .map(|parent_node_id| {
5286 let parent_hierarchy_id =
5287 NodeHierarchyItemId::from_crate_internal(Some(parent_node_id));
5288 azul_core::dom::DomNodeId {
5289 dom: dom_id,
5290 node: parent_hierarchy_id,
5291 }
5292 });
5293
5294 if let Some(parent) = parent_id {
5296 vec![node_dom_id, parent]
5297 } else {
5298 vec![node_dom_id]
5299 }
5300 }
5301
5302 #[inline]
5304 pub fn process_text_input(
5305 &mut self,
5306 text_input: &str,
5307 ) -> BTreeMap<azul_core::dom::DomNodeId, (Vec<azul_core::events::EventFilter>, bool)> {
5308 self.record_text_input(text_input)
5309 }
5310
5311 pub fn get_last_text_changeset(&self) -> Option<&PendingTextEdit> {
5313 self.text_input_manager.get_pending_changeset()
5314 }
5315
5316 pub fn get_text_before_textinput(&self, dom_id: DomId, node_id: NodeId) -> Vec<InlineContent> {
5326 if let Some(dirty_node) = self.dirty_text_nodes.get(&(dom_id, node_id)) {
5332 return dirty_node.content.clone();
5333 }
5334
5335 let layout_result = match self.layout_results.get(&dom_id) {
5338 Some(lr) => lr,
5339 None => return Vec::new(),
5340 };
5341
5342 let node_data = match layout_result
5344 .styled_dom
5345 .node_data
5346 .as_ref()
5347 .get(node_id.index())
5348 {
5349 Some(nd) => nd,
5350 None => return Vec::new(),
5351 };
5352
5353 match node_data.get_node_type() {
5355 NodeType::Text(text) => {
5356 let style = self.get_text_style_for_node(dom_id, node_id);
5358
5359 vec![InlineContent::Text(StyledRun {
5360 text: text.as_str().to_string(),
5361 style,
5362 logical_start_byte: 0,
5363 source_node_id: Some(node_id),
5364 })]
5365 }
5366 NodeType::Div | NodeType::Body | NodeType::VirtualView => {
5367 self.collect_text_from_children(dom_id, node_id)
5369 }
5370 _ => {
5371 Vec::new()
5373 }
5374 }
5375 }
5376
5377 fn get_text_style_for_node(
5379 &self,
5380 dom_id: DomId,
5381 node_id: NodeId,
5382 ) -> alloc::sync::Arc<StyleProperties> {
5383 use alloc::sync::Arc;
5384
5385 let layout_result = match self.layout_results.get(&dom_id) {
5386 Some(lr) => lr,
5387 None => return Arc::new(Default::default()),
5388 };
5389
5390 let vp = layout_result.viewport.size;
5392 let props = crate::solver3::getters::get_style_properties(
5393 &layout_result.styled_dom,
5394 node_id,
5395 self.system_style.as_ref(),
5396 azul_css::props::basic::PhysicalSize::new(vp.width, vp.height),
5397 );
5398
5399 Arc::new(props)
5400 }
5401
5402 fn collect_text_from_children(
5404 &self,
5405 dom_id: DomId,
5406 parent_node_id: NodeId,
5407 ) -> Vec<InlineContent> {
5408 let layout_result = match self.layout_results.get(&dom_id) {
5409 Some(lr) => lr,
5410 None => return Vec::new(),
5411 };
5412
5413 let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_ref();
5414 let parent_item = match node_hierarchy.get(parent_node_id.index()) {
5415 Some(item) => item,
5416 None => return Vec::new(),
5417 };
5418
5419 let mut result = Vec::new();
5420
5421 let mut current_child = parent_item.first_child_id(parent_node_id);
5423 while let Some(child_id) = current_child {
5424 let child_content = self.get_text_before_textinput(dom_id, child_id);
5426 result.extend(child_content);
5427
5428 let child_item = match node_hierarchy.get(child_id.index()) {
5430 Some(item) => item,
5431 None => break,
5432 };
5433 current_child = child_item.next_sibling_id();
5434 }
5435
5436 result
5437 }
5438
5439 pub fn extract_text_from_inline_content(&self, content: &[InlineContent]) -> String {
5443 let mut result = String::new();
5444
5445 for item in content {
5446 match item {
5447 InlineContent::Text(text_run) => {
5448 result.push_str(&text_run.text);
5449 }
5450 InlineContent::Space(_) => {
5451 result.push(' ');
5452 }
5453 InlineContent::LineBreak(_) => {
5454 result.push('\n');
5455 }
5456 InlineContent::Tab { .. } => {
5457 result.push('\t');
5458 }
5459 InlineContent::Ruby { base, .. } => {
5460 result.push_str(&self.extract_text_from_inline_content(base));
5462 }
5463 InlineContent::Marker { run, .. } => {
5464 result.push_str(&run.text);
5466 }
5467 InlineContent::Image(_) | InlineContent::Shape(_) => {}
5469 }
5470 }
5471
5472 result
5473 }
5474
5475 pub fn update_text_cache_after_edit(
5485 &mut self,
5486 dom_id: DomId,
5487 node_id: NodeId,
5488 new_inline_content: Vec<InlineContent>,
5489 ) {
5490 use crate::solver3::layout_tree::CachedInlineLayout;
5491
5492 let cursor = self.text_edit_manager.get_primary_cursor();
5494 self.dirty_text_nodes.insert(
5495 (dom_id, node_id),
5496 DirtyTextNode {
5497 content: new_inline_content.clone(),
5498 cursor,
5499 needs_ancestor_relayout: false, },
5501 );
5502
5503 let (mut constraints, ifc_layout_index) = {
5509 let layout_result = match self.layout_results.get(&dom_id) {
5510 Some(r) => r,
5511 None => {
5512 return;
5513 }
5514 };
5515
5516 let mut found: Option<(usize, &CachedInlineLayout)> = None;
5518
5519 if let Some(layout_indices) = layout_result.layout_tree.dom_to_layout.get(&node_id) {
5521 for &idx in layout_indices {
5522 if let Some(w) = layout_result.layout_tree.warm(idx) {
5523 if let Some(ref cached) = w.inline_layout_result {
5524 found = Some((idx, cached));
5525 break;
5526 }
5527 }
5528 }
5529 }
5530
5531 if found.is_none() {
5533 let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_ref();
5534 if let Some(parent_item) = node_hierarchy.get(node_id.index()) {
5535 let mut child = parent_item.first_child_id(node_id);
5536 while let Some(child_id) = child {
5537 if let Some(child_indices) = layout_result.layout_tree.dom_to_layout.get(&child_id) {
5538 for &idx in child_indices {
5539 if let Some(w) = layout_result.layout_tree.warm(idx) {
5540 if let Some(ref cached) = w.inline_layout_result {
5541 found = Some((idx, cached));
5542 break;
5543 }
5544 }
5545 }
5546 }
5547 if found.is_some() { break; }
5548 child = node_hierarchy.get(child_id.index()).and_then(|h| h.next_sibling_id());
5549 }
5550 }
5551 }
5552
5553 let (ifc_idx, cached_layout) = match found {
5554 Some(f) => {
5555 f
5556 },
5557 None => {
5558 return;
5559 }
5560 };
5561
5562 match &cached_layout.constraints {
5563 Some(c) => (c.clone(), ifc_idx),
5564 None => {
5565 return;
5566 }
5567 }
5568 };
5569
5570 if let Some(layout_result) = self.layout_results.get(&dom_id) {
5579 let mut found_width = false;
5580
5581 if let Some(layout_indices) = layout_result.layout_tree.dom_to_layout.get(&node_id) {
5583 for &idx in layout_indices {
5584 if let Some(container_node) = layout_result.layout_tree.get(idx) {
5585 if let Some(container_size) = container_node.used_size {
5586 let bp = container_node.box_props.unpack();
5587 let content_width = container_size.width
5588 - bp.padding.left - bp.padding.right
5589 - bp.border.left - bp.border.right;
5590 if content_width > 0.0 {
5591 constraints.available_width =
5592 crate::text3::cache::AvailableSpace::Definite(content_width);
5593 found_width = true;
5594 }
5595 break;
5596 }
5597 }
5598 }
5599 }
5600
5601 if !found_width {
5603 if let Some(parent_idx) = layout_result.layout_tree.get(ifc_layout_index)
5604 .and_then(|n| n.parent)
5605 {
5606 if let Some(parent_node) = layout_result.layout_tree.get(parent_idx) {
5607 if let Some(parent_size) = parent_node.used_size {
5608 let bp = parent_node.box_props.unpack();
5609 let content_width = parent_size.width
5610 - bp.padding.left - bp.padding.right
5611 - bp.border.left - bp.border.right;
5612 if content_width > 0.0 {
5613 constraints.available_width =
5614 crate::text3::cache::AvailableSpace::Definite(content_width);
5615 }
5616 }
5617 }
5618 }
5619 }
5620 }
5621
5622 let cached_snapshot = self
5631 .layout_results
5632 .get(&dom_id)
5633 .and_then(|lr| lr.layout_tree.warm(ifc_layout_index))
5634 .and_then(|w| w.inline_layout_result.as_ref())
5635 .cloned();
5636
5637 let new_layout = if let Some(cached) = cached_snapshot {
5638 self.try_incremental_text_relayout(
5639 &new_inline_content,
5640 &constraints,
5641 &cached,
5642 node_id,
5643 )
5644 .map(|(layout, _skipped_fragment)| layout)
5645 } else {
5646 self.relayout_text_node_internal(&new_inline_content, &constraints)
5647 };
5648
5649 let Some(new_layout) = new_layout else {
5650 return;
5651 };
5652
5653 if let Some(layout_result) = self.layout_results.get_mut(&dom_id) {
5656 let old_size = layout_result.layout_tree.get(ifc_layout_index).and_then(|n| n.used_size);
5657 let new_bounds = new_layout.bounds();
5658 let new_size = Some(LogicalSize {
5659 width: new_bounds.width,
5660 height: new_bounds.height,
5661 });
5662
5663 if let (Some(old), Some(new)) = (old_size, new_size) {
5665 if (old.height - new.height).abs() > 0.5 || (old.width - new.width).abs() > 0.5 {
5666 if let Some(dirty_node) = self.dirty_text_nodes.get_mut(&(dom_id, node_id)) {
5668 dirty_node.needs_ancestor_relayout = true;
5669 }
5670 }
5671 }
5672
5673 if let Some(warm_node) = layout_result.layout_tree.warm_mut(ifc_layout_index) {
5675 warm_node.inline_layout_result = Some(CachedInlineLayout::new_with_constraints(
5676 Arc::new(new_layout),
5677 constraints.available_width,
5678 false, constraints,
5680 ));
5681 }
5682 }
5683
5684 self.regenerate_display_list_for_dom(dom_id);
5688 }
5689
5690 pub fn apply_preedit_to_text_cache(&mut self, dom_id: DomId, node_id: NodeId) {
5702 let preedit = match &self.text_edit_manager.preedit_text {
5703 Some(p) if !p.is_empty() => p.clone(),
5704 _ => {
5705 self.pre_preedit_content = None;
5707 self.reapply_dirty_text_node(dom_id, node_id);
5708 return;
5709 }
5710 };
5711
5712 let cursor = match self.text_edit_manager.get_primary_cursor() {
5713 Some(c) => c,
5714 None => return,
5715 };
5716
5717 if self.pre_preedit_content.is_none() {
5720 let original = self.get_text_before_textinput(dom_id, node_id);
5721 self.pre_preedit_content = Some(original);
5722 }
5723
5724 let mut content = self.pre_preedit_content.clone().unwrap();
5726
5727 let run_idx = cursor.cluster_id.source_run as usize;
5729 let byte_pos = cursor.cluster_id.start_byte_in_run as usize;
5730 if let Some(crate::text3::cache::InlineContent::Text(run)) = content.get_mut(run_idx) {
5731 let clamped_pos = byte_pos.min(run.text.len());
5732 run.text.insert_str(clamped_pos, &preedit);
5733 }
5734
5735 self.update_text_cache_after_edit(dom_id, node_id, content);
5737 self.regenerate_display_list_for_dom(dom_id);
5738 }
5739
5740 pub fn reapply_dirty_text_node(&mut self, dom_id: DomId, node_id: NodeId) {
5741 let content = match self.dirty_text_nodes.get(&(dom_id, node_id)) {
5742 Some(dirty) => dirty.content.clone(),
5743 None => return,
5744 };
5745 self.update_text_cache_after_edit(dom_id, node_id, content);
5747 self.regenerate_display_list_for_dom(dom_id);
5749 }
5750
5751 pub fn regenerate_display_list_for_dom(&mut self, dom_id: DomId) {
5761 use crate::solver3::{
5762 display_list::generate_display_list,
5763 LayoutContext,
5764 };
5765
5766 let layout_result = match self.layout_results.get(&dom_id) {
5768 Some(lr) => lr,
5769 None => { return; }
5770 };
5771
5772 let tree = &layout_result.layout_tree;
5773 let calculated_positions = &layout_result.calculated_positions;
5774 let scroll_ids = &layout_result.scroll_ids;
5775 let styled_dom = &layout_result.styled_dom;
5776 let viewport = layout_result.viewport;
5777
5778 let scroll_offsets = self.scroll_manager.get_scroll_states_for_dom(dom_id);
5780
5781 let gpu_cache = self.gpu_state_manager.get_or_create_cache(dom_id).clone();
5783
5784 let cursor_is_visible = self.text_edit_manager.should_draw_cursor();
5786 let cursor_locations = self.text_edit_manager.build_cursor_locations();
5787 let text_selections_map = self.text_edit_manager.build_text_selections_map();
5788
5789 let mut counter_values = HashMap::new();
5791 let mut debug_messages: Option<Vec<LayoutDebugMessage>> = None;
5792 let cache_map = std::mem::take(&mut self.layout_cache.cache_map);
5793
5794 let mut ctx = LayoutContext {
5795 scrollbar_style_cache: core::cell::RefCell::new(std::collections::HashMap::new()),
5796 styled_dom,
5797 font_manager: &self.font_manager,
5798 text_selections: &text_selections_map,
5799 debug_messages: &mut debug_messages,
5800 counters: &mut counter_values,
5801 viewport_size: viewport.size,
5802 fragmentation_context: None,
5803 cursor_is_visible,
5804 cursor_locations,
5805 preedit_text: self.text_edit_manager.preedit_text.clone(),
5806 cache_map,
5807 image_cache: &self.image_cache,
5808 system_style: self.system_style.clone(),
5809 get_system_time_fn: azul_core::task::GetSystemTimeCallback {
5810 cb: azul_core::task::get_system_time_libstd,
5811 },
5812 dirty_text_overrides: BTreeMap::new(),
5813 };
5814
5815 let new_display_list = generate_display_list(
5817 &mut ctx,
5818 tree,
5819 calculated_positions,
5820 &scroll_offsets,
5821 scroll_ids,
5822 Some(&gpu_cache),
5823 &self.renderer_resources,
5824 self.id_namespace,
5825 dom_id,
5826 );
5827
5828 self.layout_cache.cache_map = std::mem::take(&mut ctx.cache_map);
5830
5831 match new_display_list {
5832 Ok(display_list) => {
5833 if let Some(layout_result) = self.layout_results.get_mut(&dom_id) {
5834 layout_result.display_list = display_list;
5835 }
5836 #[cfg(feature = "a11y")]
5839 self.update_a11y_tree_incremental();
5840 }
5841 Err(_e) => {
5842 }
5843 }
5844 }
5845
5846 fn relayout_text_node_internal(
5848 &self,
5849 content: &[InlineContent],
5850 constraints: &UnifiedConstraints,
5851 ) -> Option<UnifiedLayout> {
5852 let (logical_items, shaped_items) = self.shape_text_for_relayout(content, constraints)?;
5853
5854 if logical_items.is_empty() {
5855 return Some(UnifiedLayout {
5856 items: Vec::new(),
5857 overflow: crate::text3::cache::OverflowInfo::default(),
5858 });
5859 }
5860
5861 self.fragment_layout_from_shaped(&logical_items, &shaped_items, constraints)
5862 }
5863
5864 fn shape_text_for_relayout(
5868 &self,
5869 content: &[InlineContent],
5870 constraints: &UnifiedConstraints,
5871 ) -> Option<(
5872 Vec<crate::text3::cache::LogicalItem>,
5873 Vec<crate::text3::cache::ShapedItem>,
5874 )> {
5875 use crate::text3::cache::{
5876 create_logical_items, reorder_logical_items, shape_visual_items, BidiDirection,
5877 };
5878
5879 let logical_items = create_logical_items(content, &[], &mut None);
5880 if logical_items.is_empty() {
5881 return Some((logical_items, Vec::new()));
5882 }
5883
5884 let base_direction = constraints.direction.unwrap_or(BidiDirection::Ltr);
5885 let visual_items = reorder_logical_items(
5886 &logical_items,
5887 base_direction,
5888 crate::text3::cache::UnicodeBidi::Normal,
5889 &mut None,
5890 )
5891 .ok()?;
5892
5893 let loaded_fonts = self.font_manager.get_loaded_fonts();
5894 let shaped_items = shape_visual_items(
5895 &visual_items,
5896 self.font_manager.get_font_chain_cache(),
5897 &self.font_manager.fc_cache,
5898 &loaded_fonts,
5899 &mut None,
5900 )
5901 .ok()?;
5902
5903 Some((logical_items, shaped_items))
5904 }
5905
5906 fn fragment_layout_from_shaped(
5908 &self,
5909 logical_items: &[crate::text3::cache::LogicalItem],
5910 shaped_items: &[crate::text3::cache::ShapedItem],
5911 constraints: &UnifiedConstraints,
5912 ) -> Option<UnifiedLayout> {
5913 use crate::text3::cache::{perform_fragment_layout, BreakCursor};
5914
5915 let loaded_fonts = self.font_manager.get_loaded_fonts();
5916 let mut cursor = BreakCursor::new(shaped_items);
5917 perform_fragment_layout(&mut cursor, logical_items, constraints, &mut None, &loaded_fonts).ok()
5918 }
5919
5920 fn try_incremental_text_relayout(
5934 &self,
5935 content: &[InlineContent],
5936 constraints: &UnifiedConstraints,
5937 cached: &crate::solver3::layout_tree::CachedInlineLayout,
5938 edited_node_id: NodeId,
5939 ) -> Option<(UnifiedLayout, bool)> {
5940 use crate::text3::cache::{
5941 try_incremental_relayout as decide_incremental,
5942 IncrementalRelayoutResult, PositionedItem, ShapedItem,
5943 };
5944
5945 let (logical_items, shaped_items) = self.shape_text_for_relayout(content, constraints)?;
5946
5947 if logical_items.is_empty() {
5948 return Some((
5949 UnifiedLayout {
5950 items: Vec::new(),
5951 overflow: crate::text3::cache::OverflowInfo::default(),
5952 },
5953 true,
5954 ));
5955 }
5956
5957 let incremental_ok = cached.line_breaks.is_some()
5964 && cached.layout.overflow.overflow_items.is_empty()
5965 && shaped_items.len() == cached.layout.items.len();
5966
5967 if incremental_ok {
5968 let line_breaks = cached.line_breaks.as_ref().unwrap();
5969
5970 let old_advances: Vec<f32> =
5971 cached.item_metrics.iter().map(|m| m.advance_width).collect();
5972 let new_advances: Vec<f32> =
5973 shaped_items.iter().map(|si| si.bounds().width).collect();
5974
5975 let mut dirty_indices: Vec<usize> = Vec::new();
5980 for (i, (old_a, new_a)) in old_advances.iter().zip(new_advances.iter()).enumerate() {
5981 if (new_a - old_a).abs() > 0.01 {
5982 dirty_indices.push(i);
5983 }
5984 }
5985 for (i, si) in shaped_items.iter().enumerate() {
5986 if let ShapedItem::Cluster(c) = si {
5987 if c.source_node_id == Some(edited_node_id)
5988 && !dirty_indices.contains(&i)
5989 {
5990 dirty_indices.push(i);
5991 }
5992 }
5993 }
5994 dirty_indices.sort_unstable();
5995 dirty_indices.dedup();
5996
5997 let decision =
5998 decide_incremental(&dirty_indices, &old_advances, &new_advances, line_breaks);
5999
6000 match decision {
6001 IncrementalRelayoutResult::GlyphSwap => {
6002 let items: Vec<PositionedItem> = cached
6006 .layout
6007 .items
6008 .iter()
6009 .zip(shaped_items.into_iter())
6010 .map(|(old_positioned, new_shaped)| PositionedItem {
6011 item: new_shaped,
6012 position: old_positioned.position,
6013 line_index: old_positioned.line_index,
6014 })
6015 .collect();
6016 return Some((
6017 UnifiedLayout {
6018 items,
6019 overflow: cached.layout.overflow.clone(),
6020 },
6021 true,
6022 ));
6023 }
6024 IncrementalRelayoutResult::LineShift {
6025 affected_item,
6026 delta,
6027 } => {
6028 let affected_line = cached.layout.items[affected_item].line_index;
6032 let items: Vec<PositionedItem> = cached
6033 .layout
6034 .items
6035 .iter()
6036 .zip(shaped_items.into_iter())
6037 .enumerate()
6038 .map(|(i, (old_positioned, new_shaped))| {
6039 let mut position = old_positioned.position;
6040 if i > affected_item && old_positioned.line_index == affected_line {
6041 position.x += delta;
6042 }
6043 PositionedItem {
6044 item: new_shaped,
6045 position,
6046 line_index: old_positioned.line_index,
6047 }
6048 })
6049 .collect();
6050 return Some((
6051 UnifiedLayout {
6052 items,
6053 overflow: cached.layout.overflow.clone(),
6054 },
6055 true,
6056 ));
6057 }
6058 IncrementalRelayoutResult::PartialReflow { .. }
6059 | IncrementalRelayoutResult::FullRelayout => {
6060 }
6062 }
6063 }
6064
6065 let layout = self.fragment_layout_from_shaped(&logical_items, &shaped_items, constraints)?;
6069 Some((layout, false))
6070 }
6071
6072 #[cfg(feature = "a11y")]
6074 fn get_node_used_size_a11y(
6075 &self,
6076 dom_id: DomId,
6077 node_id: NodeId,
6078 ) -> Option<azul_core::geom::LogicalSize> {
6079 let layout_result = self.layout_results.get(&dom_id)?;
6080 let layout_indices = layout_result.layout_tree.dom_to_layout.get(&node_id)?;
6081 let idx = *layout_indices.first()?;
6082 let node = layout_result.layout_tree.get(idx)?;
6083 node.used_size
6084 }
6085
6086 pub fn get_node_bounds(
6088 &self,
6089 dom_id: DomId,
6090 node_id: NodeId,
6091 ) -> Option<azul_css::props::basic::LayoutRect> {
6092 use azul_css::props::basic::LayoutRect;
6093
6094 let layout_result = self.layout_results.get(&dom_id)?;
6095 let layout_indices = layout_result.layout_tree.dom_to_layout.get(&node_id)?;
6096 let idx = *layout_indices.first()?;
6097 let node = layout_result.layout_tree.get(idx)?;
6098
6099 let size = node.used_size?;
6101
6102 let position = layout_result.calculated_positions.get(idx)?;
6104
6105 Some(LayoutRect {
6106 origin: azul_css::props::basic::LayoutPoint {
6107 x: position.x as f32 as isize,
6108 y: position.y as f32 as isize,
6109 },
6110 size: azul_css::props::basic::LayoutSize {
6111 width: size.width as isize,
6112 height: size.height as isize,
6113 },
6114 })
6115 }
6116
6117 #[cfg(feature = "a11y")]
6119 fn scroll_to_node_if_needed(
6120 &mut self,
6121 dom_id: DomId,
6122 node_id: NodeId,
6123 now: std::time::Instant,
6124 ) {
6125 let Some(target_bounds) = self.get_node_bounds(dom_id, node_id) else {
6127 return;
6128 };
6129
6130 let dom_node_id = azul_core::dom::DomNodeId {
6132 dom: dom_id,
6133 node: azul_core::styled_dom::NodeHierarchyItemId::from_crate_internal(Some(node_id)),
6134 };
6135 let Some(scroll_ancestor) = self.find_scrollable_ancestor(dom_node_id) else {
6136 return;
6137 };
6138 let Some(scroll_node_id) = scroll_ancestor.node.into_crate_internal() else {
6139 return;
6140 };
6141 let Some(ancestor_bounds) = self.get_node_bounds(dom_id, scroll_node_id) else {
6142 return;
6143 };
6144
6145 let current_scroll = self
6146 .scroll_manager
6147 .get_current_offset(dom_id, scroll_node_id)
6148 .unwrap_or_default();
6149
6150 let vp_x = ancestor_bounds.origin.x as f32 + current_scroll.x;
6152 let vp_y = ancestor_bounds.origin.y as f32 + current_scroll.y;
6153 let vp_w = ancestor_bounds.size.width as f32;
6154 let vp_h = ancestor_bounds.size.height as f32;
6155
6156 let target_x = target_bounds.origin.x as f32;
6157 let target_y = target_bounds.origin.y as f32;
6158 let target_w = target_bounds.size.width as f32;
6159 let target_h = target_bounds.size.height as f32;
6160
6161 let visible_x = target_x >= vp_x && (target_x + target_w) <= (vp_x + vp_w);
6162 let visible_y = target_y >= vp_y && (target_y + target_h) <= (vp_y + vp_h);
6163
6164 if visible_x && visible_y {
6165 return; }
6167
6168 let mut scroll_x = current_scroll.x;
6170 let mut scroll_y = current_scroll.y;
6171
6172 if target_x < vp_x {
6173 scroll_x = target_x - ancestor_bounds.origin.x as f32;
6174 } else if (target_x + target_w) > (vp_x + vp_w) {
6175 scroll_x = (target_x + target_w) - ancestor_bounds.origin.x as f32 - vp_w;
6176 }
6177
6178 if target_y < vp_y {
6179 scroll_y = target_y - ancestor_bounds.origin.y as f32;
6180 } else if (target_y + target_h) > (vp_y + vp_h) {
6181 scroll_y = (target_y + target_h) - ancestor_bounds.origin.y as f32 - vp_h;
6182 }
6183
6184 self.scroll_manager.scroll_to(
6185 dom_id,
6186 scroll_node_id,
6187 LogicalPosition { x: scroll_x, y: scroll_y },
6188 std::time::Duration::from_millis(300).into(),
6189 azul_core::events::EasingFunction::EaseOut,
6190 now.into(),
6191 );
6192 }
6193
6194 fn scroll_cursor_into_view_if_needed(
6207 &mut self,
6208 dom_id: DomId,
6209 node_id: NodeId,
6210 now: std::time::Instant,
6211 ) {
6212 let Some(cursor) = self.text_edit_manager.get_primary_cursor() else {
6214 return;
6215 };
6216
6217 let Some(inline_layout) = self.get_node_inline_layout(dom_id, node_id) else {
6219 return;
6220 };
6221
6222 let Some(cursor_rect) = inline_layout.get_cursor_rect(&cursor) else {
6224 return;
6225 };
6226
6227 let Some(node_bounds) = self.get_node_bounds(dom_id, node_id) else {
6229 return;
6230 };
6231
6232 let cursor_abs_x = node_bounds.origin.x as f32 + cursor_rect.origin.x;
6234 let cursor_abs_y = node_bounds.origin.y as f32 + cursor_rect.origin.y;
6235
6236 let dom_node_id = azul_core::dom::DomNodeId {
6238 dom: dom_id,
6239 node: azul_core::styled_dom::NodeHierarchyItemId::from_crate_internal(Some(node_id)),
6240 };
6241 let scroll_ancestor = match self.find_scrollable_ancestor(dom_node_id) {
6242 Some(a) => a,
6243 None => return, };
6245 let scroll_node_id = match scroll_ancestor.node.into_crate_internal() {
6246 Some(id) => id,
6247 None => return,
6248 };
6249
6250 let Some(ancestor_bounds) = self.get_node_bounds(dom_id, scroll_node_id) else {
6252 return;
6253 };
6254 let current_scroll = self
6255 .scroll_manager
6256 .get_current_offset(dom_id, scroll_node_id)
6257 .unwrap_or_default();
6258
6259 let viewport_x = ancestor_bounds.origin.x as f32 + current_scroll.x;
6261 let viewport_y = ancestor_bounds.origin.y as f32 + current_scroll.y;
6262 let viewport_width = ancestor_bounds.size.width as f32;
6263 let viewport_height = ancestor_bounds.size.height as f32;
6264
6265 let cursor_visible_x = (cursor_abs_x as f32) >= viewport_x
6267 && (cursor_abs_x as f32) <= viewport_x + viewport_width;
6268 let cursor_visible_y = (cursor_abs_y as f32) >= viewport_y
6269 && (cursor_abs_y as f32) <= viewport_y + viewport_height;
6270
6271 if cursor_visible_x && cursor_visible_y {
6272 return;
6274 }
6275
6276 let mut target_scroll_x = current_scroll.x;
6278 let mut target_scroll_y = current_scroll.y;
6279
6280 if (cursor_abs_x as f32) < viewport_x {
6282 target_scroll_x = cursor_abs_x as f32 - ancestor_bounds.origin.x as f32;
6283 } else if (cursor_abs_x as f32) > viewport_x + viewport_width {
6284 target_scroll_x = cursor_abs_x as f32 - ancestor_bounds.origin.x as f32 - viewport_width
6285 + cursor_rect.size.width;
6286 }
6287
6288 if (cursor_abs_y as f32) < viewport_y {
6290 target_scroll_y = cursor_abs_y as f32 - ancestor_bounds.origin.y as f32;
6291 } else if (cursor_abs_y as f32) > viewport_y + viewport_height {
6292 target_scroll_y = cursor_abs_y as f32 - ancestor_bounds.origin.y as f32 - viewport_height
6293 + cursor_rect.size.height;
6294 }
6295
6296 self.scroll_manager.scroll_to(
6298 dom_id,
6299 scroll_node_id,
6300 LogicalPosition {
6301 x: target_scroll_x,
6302 y: target_scroll_y,
6303 },
6304 std::time::Duration::from_millis(200).into(),
6305 azul_core::events::EasingFunction::EaseOut,
6306 now.into(),
6307 );
6308 }
6309
6310 fn byte_offset_to_cursor(
6325 &self,
6326 text_layout: &UnifiedLayout,
6327 byte_offset: u32,
6328 ) -> Option<TextCursor> {
6329 if byte_offset == 0 {
6331 for item in &text_layout.items {
6333 if let ShapedItem::Cluster(cluster) = &item.item {
6334 return Some(TextCursor {
6335 cluster_id: cluster.source_cluster_id,
6336 affinity: CursorAffinity::Trailing,
6337 });
6338 }
6339 }
6340 return Some(TextCursor {
6342 cluster_id: GraphemeClusterId {
6343 source_run: 0,
6344 start_byte_in_run: 0,
6345 },
6346 affinity: CursorAffinity::Trailing,
6347 });
6348 }
6349
6350 let mut current_byte_offset = 0u32;
6352
6353 for item in &text_layout.items {
6354 if let ShapedItem::Cluster(cluster) = &item.item {
6355 let cluster_byte_length = cluster.text.len() as u32;
6357 let cluster_end_byte = current_byte_offset + cluster_byte_length;
6358
6359 if byte_offset >= current_byte_offset && byte_offset <= cluster_end_byte {
6361 return Some(TextCursor {
6363 cluster_id: cluster.source_cluster_id,
6364 affinity: CursorAffinity::Trailing,
6365 });
6366 }
6367
6368 current_byte_offset = cluster_end_byte;
6369 }
6370 }
6371
6372 for item in text_layout.items.iter().rev() {
6374 if let ShapedItem::Cluster(cluster) = &item.item {
6375 return Some(TextCursor {
6376 cluster_id: cluster.source_cluster_id,
6377 affinity: CursorAffinity::Trailing,
6378 });
6379 }
6380 }
6381
6382 Some(TextCursor {
6384 cluster_id: GraphemeClusterId {
6385 source_run: 0,
6386 start_byte_in_run: 0,
6387 },
6388 affinity: CursorAffinity::Trailing,
6389 })
6390 }
6391
6392 fn get_node_inline_layout(
6397 &self,
6398 dom_id: DomId,
6399 node_id: NodeId,
6400 ) -> Option<alloc::sync::Arc<UnifiedLayout>> {
6401 let layout_tree = self.layout_cache.tree.as_ref()?;
6403
6404 let layout_idx = layout_tree
6406 .nodes
6407 .iter()
6408 .position(|node| node.dom_node_id == Some(node_id))?;
6409
6410 layout_tree.warm(layout_idx)?
6412 .inline_layout_result
6413 .as_ref()
6414 .map(|c| c.clone_layout())
6415 }
6416
6417 #[must_use = "Returned nodes must be marked dirty for re-layout"]
6433 #[cfg(feature = "a11y")]
6434 pub fn edit_text_node(
6435 &mut self,
6436 dom_id: DomId,
6437 node_id: NodeId,
6438 edit_type: TextEditType,
6439 ) -> Vec<azul_core::dom::DomNodeId> {
6440 use crate::managers::text_input::TextInputSource;
6441
6442 let text_input = match &edit_type {
6444 TextEditType::ReplaceSelection(text) => text.clone(),
6445 TextEditType::SetValue(text) => text.clone(),
6446 TextEditType::SetNumericValue(value) => value.to_string(),
6447 };
6448
6449 let old_inline_content = self.get_text_before_textinput(dom_id, node_id);
6451 let old_text = self.extract_text_from_inline_content(&old_inline_content);
6452
6453 let hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(node_id));
6455 let dom_node_id = azul_core::dom::DomNodeId {
6456 dom: dom_id,
6457 node: hierarchy_id,
6458 };
6459
6460 self.text_input_manager.record_input(
6462 dom_node_id,
6463 text_input,
6464 old_text,
6465 TextInputSource::Accessibility, );
6467
6468 self.apply_text_changeset().dirty_nodes
6470 }
6471
6472 #[cfg(not(feature = "a11y"))]
6473 pub fn process_accessibility_action(
6474 &mut self,
6475 _dom_id: DomId,
6476 _node_id: NodeId,
6477 _action: azul_core::dom::AccessibilityAction,
6478 _now: std::time::Instant,
6479 ) -> BTreeMap<DomNodeId, (Vec<azul_core::events::EventFilter>, bool)> {
6480 BTreeMap::new()
6482 }
6483
6484 pub fn process_mouse_click_for_selection(
6512 &mut self,
6513 position: azul_core::geom::LogicalPosition,
6514 time_ms: u64,
6515 ) -> Option<Vec<azul_core::dom::DomNodeId>> {
6516 use crate::managers::hover::InputPointId;
6517 use crate::text3::selection::{select_paragraph_at_cursor, select_word_at_cursor};
6518
6519 let mut found_selection: Option<(DomId, NodeId, SelectionRange, azul_core::geom::LogicalPosition)> = None;
6523
6524 if let Some(hit_test) = self.hover_manager.get_current(&InputPointId::Mouse) {
6526 for (dom_id, hit) in &hit_test.hovered_nodes {
6528 let layout_result = match self.layout_results.get(dom_id) {
6529 Some(lr) => lr,
6530 None => continue,
6531 };
6532 let tree = &layout_result.layout_tree;
6534
6535 let node_hierarchy = layout_result.styled_dom.node_hierarchy.as_container();
6539 let get_dom_depth = |node_id: &NodeId| -> usize {
6540 let mut depth = 0;
6541 let mut current = *node_id;
6542 while let Some(parent) = node_hierarchy.get(current).and_then(|h| h.parent_id()) {
6543 depth += 1;
6544 current = parent;
6545 }
6546 depth
6547 };
6548
6549 let mut sorted_hits: Vec<_> = hit.regular_hit_test_nodes.iter().collect();
6550 sorted_hits.sort_by(|(a_id, _), (b_id, _)| {
6551 let depth_a = get_dom_depth(a_id);
6552 let depth_b = get_dom_depth(b_id);
6553 depth_b.cmp(&depth_a).then_with(|| a_id.index().cmp(&b_id.index()))
6556 });
6557
6558 for (node_id, hit_item) in sorted_hits {
6559 if !self.is_text_selectable(&layout_result.styled_dom, *node_id) {
6561 continue;
6562 }
6563
6564 let layout_node_idx = tree.nodes.iter().position(|n| n.dom_node_id == Some(*node_id));
6566 let layout_node_idx = match layout_node_idx {
6567 Some(idx) => idx,
6568 None => continue,
6569 };
6570 let warm_node = match tree.warm(layout_node_idx) {
6571 Some(w) => w,
6572 None => continue,
6573 };
6574
6575 let (cached_layout, ifc_root_node_id) = if let Some(ref cached) = warm_node.inline_layout_result {
6578 (cached, *node_id)
6580 } else if let Some(ref membership) = warm_node.ifc_membership {
6581 match tree.warm(membership.ifc_root_layout_index) {
6583 Some(ifc_root_warm) => match (ifc_root_warm.inline_layout_result.as_ref(), tree.get(membership.ifc_root_layout_index).and_then(|n| n.dom_node_id)) {
6584 (Some(cached), Some(root_dom_id)) => (cached, root_dom_id),
6585 _ => continue,
6586 },
6587 None => continue,
6588 }
6589 } else {
6590 continue;
6592 };
6593
6594 let layout = &cached_layout.layout;
6595
6596 let local_pos = hit_item.point_relative_to_item;
6599
6600 if let Some(cursor) = layout.hittest_cursor(local_pos) {
6602 found_selection = Some((*dom_id, ifc_root_node_id, SelectionRange {
6604 start: cursor.clone(),
6605 end: cursor,
6606 }, local_pos));
6607 break;
6608 }
6609 }
6610
6611 if found_selection.is_some() {
6612 break;
6613 }
6614 }
6615 }
6616
6617 if found_selection.is_none() {
6620 for (dom_id, layout_result) in &self.layout_results {
6621 let tree = &layout_result.layout_tree;
6625
6626 for (node_idx, layout_node) in tree.nodes.iter().enumerate() {
6628 let warm = match tree.warm(node_idx) {
6629 Some(w) => w,
6630 None => continue,
6631 };
6632 let cached_layout = match warm.inline_layout_result.as_ref() {
6633 Some(c) => c,
6634 None => continue, };
6636
6637 let node_id = match layout_node.dom_node_id {
6638 Some(n) => n,
6639 None => continue,
6640 };
6641
6642 if !self.is_text_selectable(&layout_result.styled_dom, node_id) {
6644 continue;
6645 }
6646
6647 let node_pos = layout_result.calculated_positions
6650 .get(node_idx)
6651 .copied()
6652 .unwrap_or_default();
6653
6654 let node_size = layout_node.used_size.unwrap_or_else(|| {
6656 let bounds = cached_layout.layout.bounds();
6657 azul_core::geom::LogicalSize::new(bounds.width, bounds.height)
6658 });
6659
6660 if position.x < node_pos.x || position.x > node_pos.x + node_size.width ||
6661 position.y < node_pos.y || position.y > node_pos.y + node_size.height {
6662 continue;
6663 }
6664
6665 let local_pos = azul_core::geom::LogicalPosition {
6667 x: position.x - node_pos.x,
6668 y: position.y - node_pos.y,
6669 };
6670
6671 let layout = &cached_layout.layout;
6672
6673 if let Some(cursor) = layout.hittest_cursor(local_pos) {
6675 found_selection = Some((*dom_id, node_id, SelectionRange {
6676 start: cursor.clone(),
6677 end: cursor,
6678 }, local_pos));
6679 break;
6680 }
6681 }
6682
6683 if found_selection.is_some() {
6684 break;
6685 }
6686 }
6687 }
6688
6689 let (dom_id, ifc_root_node_id, initial_range, _local_pos) = found_selection?;
6690
6691 let node_hierarchy_id = NodeHierarchyItemId::from_crate_internal(Some(ifc_root_node_id));
6694 let dom_node_id = azul_core::dom::DomNodeId {
6695 dom: dom_id,
6696 node: node_hierarchy_id,
6697 };
6698
6699 let click_count = self.gesture_drag_manager.detect_click_count();
6702
6703 let final_range = if click_count > 1 {
6705 let layout_result = self.layout_results.get(&dom_id)?;
6707 let tree = &layout_result.layout_tree;
6708
6709 let layout_idx = tree.nodes.iter().position(|n| n.dom_node_id == Some(ifc_root_node_id))?;
6711 let cached_layout = tree.warm(layout_idx)?.inline_layout_result.as_ref()?;
6712 let layout = &cached_layout.layout;
6713
6714 match click_count {
6715 2 => select_word_at_cursor(&initial_range.start, layout.as_ref())
6716 .unwrap_or(initial_range),
6717 3 => select_paragraph_at_cursor(&initial_range.start, layout.as_ref())
6718 .unwrap_or(initial_range),
6719 _ => initial_range,
6720 }
6721 } else {
6722 initial_range
6723 };
6724
6725 let is_contenteditable = self.layout_results.get(&dom_id)
6733 .map(|lr| {
6734 let node_hierarchy = lr.styled_dom.node_hierarchy.as_container();
6735 let node_data = lr.styled_dom.node_data.as_ref();
6736
6737 let mut current_node = Some(ifc_root_node_id);
6739 while let Some(node_id) = current_node {
6740 if let Some(styled_node) = node_data.get(node_id.index()) {
6741 if styled_node.is_contenteditable() {
6745 return true;
6746 }
6747
6748 let has_contenteditable_attr = styled_node.attributes().as_ref().iter().any(|attr| {
6750 matches!(attr, azul_core::dom::AttributeType::ContentEditable(_))
6751 });
6752 if has_contenteditable_attr {
6753 return true;
6754 }
6755 }
6756 current_node = node_hierarchy.get(node_id).and_then(|h| h.parent_id());
6758 }
6759 false
6760 })
6761 .unwrap_or(false);
6762
6763 let ce_key = self.layout_results.get(&dom_id).map(|lr| {
6771 azul_core::diff::calculate_contenteditable_key(
6772 lr.styled_dom.node_data.as_ref(),
6773 lr.styled_dom.node_hierarchy.as_ref(),
6774 ifc_root_node_id,
6775 )
6776 }).unwrap_or(0);
6777 self.text_edit_manager.initialize_editing(
6778 final_range.start, dom_id, ifc_root_node_id, ce_key,
6779 );
6780 let now = azul_core::task::Instant::now();
6781 self.text_edit_manager.blink.reset_blink_on_input(now.clone());
6782 self.text_edit_manager.blink.set_blink_timer_active(true);
6783 self.regenerate_display_list_for_dom(dom_id);
6788
6789 Some(vec![dom_node_id])
6791 }
6792
6793 pub fn process_mouse_drag_for_selection(
6810 &mut self,
6811 _start_position: azul_core::geom::LogicalPosition,
6812 current_position: azul_core::geom::LogicalPosition,
6813 ) -> Option<Vec<azul_core::dom::DomNodeId>> {
6814 use azul_core::selection::{Selection, SelectionRange};
6815
6816 let mc = self.text_edit_manager.multi_cursor.as_ref()?;
6821 let anchor = match &mc.get_primary()?.selection {
6822 Selection::Cursor(c) => *c,
6823 Selection::Range(r) => r.start, };
6825 let dom_id = mc.node_id.dom;
6826 let node_id = mc.node_id.node.into_crate_internal()?;
6827 let dom_node_id = mc.node_id;
6828
6829 let layout_result = self.layout_results.get(&dom_id)?;
6831 let tree = &layout_result.layout_tree;
6832 let layout_idx = tree.nodes.iter()
6833 .position(|n| n.dom_node_id == Some(node_id))?;
6834 let node_pos = layout_result.calculated_positions
6835 .get(layout_idx)
6836 .copied()
6837 .unwrap_or_default();
6838 let cached = tree.warm(layout_idx)?.inline_layout_result.as_ref()?;
6839
6840 let local_pos = azul_core::geom::LogicalPosition {
6841 x: current_position.x - node_pos.x,
6842 y: current_position.y - node_pos.y,
6843 };
6844 let focus = cached.layout.hittest_cursor(local_pos)?;
6845
6846 let mc = self.text_edit_manager.multi_cursor.as_mut()?;
6848 if let Some(primary) = mc.get_primary_mut() {
6849 if anchor == focus {
6850 primary.selection = Selection::Cursor(anchor);
6851 } else {
6852 primary.selection = Selection::Range(SelectionRange {
6853 start: anchor,
6854 end: focus,
6855 });
6856 }
6857 }
6858
6859 self.text_edit_manager.mark_dirty();
6860 self.regenerate_display_list_for_dom(dom_id);
6861 Some(vec![dom_node_id])
6862 }
6863
6864 pub fn delete_selection(
6878 &mut self,
6879 target: azul_core::dom::DomNodeId,
6880 forward: bool,
6881 ) -> Option<Vec<azul_core::dom::DomNodeId>> {
6882 let dom_id = target.dom;
6883 let node_id = target.node.into_crate_internal()?;
6884
6885 let current_selections = if let Some(ref mc) = self.text_edit_manager.multi_cursor {
6887 mc.to_selections()
6888 } else if let Some(cursor) = self.text_edit_manager.get_primary_cursor() {
6889 vec![Selection::Cursor(cursor)]
6890 } else {
6891 return None;
6892 };
6893
6894 let content = self.get_text_before_textinput(dom_id, node_id);
6895 let edit = if forward {
6896 crate::text3::edit::TextEdit::DeleteForward
6897 } else {
6898 crate::text3::edit::TextEdit::DeleteBackward
6899 };
6900 let (new_content, new_selections) = crate::text3::edit::edit_text(
6901 &content, ¤t_selections, &edit,
6902 );
6903
6904 if let Some(ref mut mc) = self.text_edit_manager.multi_cursor {
6906 mc.update_from_edit_result(&new_selections);
6907 }
6908 self.update_text_cache_after_edit(dom_id, node_id, new_content);
6911 self.regenerate_display_list_for_dom(dom_id);
6912
6913 Some(vec![target])
6914 }
6915
6916 pub fn get_selected_content_for_clipboard(
6932 &self,
6933 dom_id: &DomId,
6934 ) -> Option<crate::managers::selection::ClipboardContent> {
6935 use crate::{
6936 managers::selection::{ClipboardContent, StyledTextRun},
6937 text3::cache::ShapedItem,
6938 };
6939
6940 let ranges = if let Some(ref mc) = self.text_edit_manager.multi_cursor {
6942 mc.selections.iter().filter_map(|s| match &s.selection {
6943 azul_core::selection::Selection::Range(r) => Some(*r),
6944 _ => None,
6945 }).collect::<Vec<_>>()
6946 } else {
6947 Vec::new()
6948 };
6949 if ranges.is_empty() {
6950 return None;
6951 }
6952
6953 let _ = ranges;
6958 None
6959 }
6960
6961 pub fn process_image_callback_updates(
6976 &mut self,
6977 image_callbacks_changed: &BTreeMap<DomId, FastBTreeSet<NodeId>>,
6978 gl_context: &OptionGlContextPtr,
6979 ) -> Vec<(DomId, NodeId, azul_core::gl::Texture)> {
6980 use crate::callbacks::{RenderImageCallback, RenderImageCallbackInfo};
6981
6982 let mut updated_textures = Vec::new();
6983
6984 for (dom_id, node_ids) in image_callbacks_changed {
6985 let layout_result = match self.layout_results.get_mut(dom_id) {
6986 Some(lr) => lr,
6987 None => continue,
6988 };
6989
6990 for node_id in node_ids {
6991 let node_data_container = layout_result.styled_dom.node_data.as_container();
6993 let node_data = match node_data_container.get(*node_id) {
6994 Some(nd) => nd,
6995 None => continue,
6996 };
6997
6998 let has_callback = matches!(node_data.get_node_type(), NodeType::Image(img_ref)
7000 if img_ref.get_image_callback().is_some());
7001
7002 if !has_callback {
7003 continue;
7004 }
7005
7006 let layout_indices = match layout_result.layout_tree.dom_to_layout.get(node_id) {
7009 Some(indices) if !indices.is_empty() => indices,
7010 _ => continue,
7011 };
7012
7013 let layout_index = layout_indices[0];
7015
7016 let position = match layout_result.calculated_positions.get(layout_index) {
7018 Some(pos) => *pos,
7019 None => continue,
7020 };
7021
7022 let layout_node = match layout_result.layout_tree.get(layout_index) {
7024 Some(ln) => ln,
7025 None => continue,
7026 };
7027
7028 let (width, height) = match layout_node.used_size {
7030 Some(size) => (size.width, size.height),
7031 None => continue, };
7033
7034 let callback_domnode_id = DomNodeId {
7035 dom: *dom_id,
7036 node: azul_core::styled_dom::NodeHierarchyItemId::from_crate_internal(Some(
7037 *node_id,
7038 )),
7039 };
7040
7041 let bounds = HidpiAdjustedBounds::from_bounds(
7042 azul_css::props::basic::LayoutSize {
7043 width: width as isize,
7044 height: height as isize,
7045 },
7046 self.current_window_state.size.get_hidpi_factor(),
7047 );
7048
7049 let mut gl_callback_info = RenderImageCallbackInfo::new(
7051 callback_domnode_id,
7052 bounds,
7053 gl_context,
7054 &self.image_cache,
7055 &self.font_manager.fc_cache,
7056 );
7057
7058 let new_image_ref = {
7060 let mut node_data_mut = layout_result.styled_dom.node_data.as_container_mut();
7061 match node_data_mut.get_mut(*node_id) {
7062 Some(nd) => {
7063 match &mut nd.node_type {
7064 NodeType::Image(ref mut img_ref) => {
7065 let callback_result = img_ref.as_mut().get_image_callback_mut();
7067
7068 if callback_result.is_none() {
7069 match img_ref.get_data() {
7073 azul_core::resources::DecodedImage::Callback(core_callback) => {
7074 if core_callback.callback.cb == 0 {
7075 None
7076 } else {
7077 let callback = RenderImageCallback::from_core(&core_callback.callback);
7078 let refany_clone = core_callback.refany.clone();
7079 use std::panic;
7080 let result = panic::catch_unwind(panic::AssertUnwindSafe(|| {
7081 (callback.cb)(refany_clone, gl_callback_info)
7082 }));
7083 result.ok()
7084 }
7085 }
7086 _ => None,
7087 }
7088 } else {
7089 callback_result.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 }
7101 _ => None,
7102 }
7103 }
7104 None => None,
7105 }
7106 };
7107
7108 #[cfg(feature = "gl_context_loader")]
7110 if let Some(gl) = gl_context.as_ref() {
7111 use gl_context_loader::gl;
7112 gl.bind_framebuffer(gl::FRAMEBUFFER, 0);
7113 gl.disable(gl::FRAMEBUFFER_SRGB);
7114 gl.disable(gl::MULTISAMPLE);
7115 }
7116
7117 if let Some(image_ref) = new_image_ref {
7119 if let Some(decoded_image) = image_ref.into_inner() {
7120 if let azul_core::resources::DecodedImage::Gl(texture) = decoded_image {
7121 updated_textures.push((*dom_id, *node_id, texture));
7122 }
7123 }
7124 }
7125 }
7126 }
7127
7128 updated_textures
7129 }
7130
7131 pub fn check_and_queue_virtual_view_reinvoke(
7140 &mut self,
7141 dom_id: DomId,
7142 node_id: NodeId,
7143 ) -> bool {
7144 let bounds = match Self::get_virtual_view_bounds_from_layout(
7146 &self.layout_results,
7147 dom_id,
7148 node_id,
7149 ) {
7150 Some(b) => b,
7151 None => return false, };
7153
7154 let reason = self.virtual_view_manager.check_reinvoke(
7156 dom_id, node_id, &self.scroll_manager, bounds,
7157 );
7158
7159 if reason.is_some() {
7160 self.pending_virtual_view_updates
7162 .entry(dom_id)
7163 .or_insert_with(FastBTreeSet::new)
7164 .insert(node_id);
7165 true
7166 } else {
7167 false
7168 }
7169 }
7170
7171 pub fn process_virtual_view_updates(
7188 &mut self,
7189 vviews_to_update: &BTreeMap<DomId, FastBTreeSet<NodeId>>,
7190 window_state: &FullWindowState,
7191 renderer_resources: &RendererResources,
7192 system_callbacks: &ExternalSystemCallbacks,
7193 ) -> Vec<(DomId, NodeId)> {
7194 let mut updated_vviews = Vec::new();
7195
7196 for (dom_id, node_ids) in vviews_to_update {
7197 for node_id in node_ids {
7198 let bounds = match Self::get_virtual_view_bounds_from_layout(
7200 &self.layout_results,
7201 *dom_id,
7202 *node_id,
7203 ) {
7204 Some(b) => b,
7205 None => continue,
7206 };
7207
7208 self.virtual_view_manager.force_reinvoke(*dom_id, *node_id);
7210
7211 if let Some(_child_dom_id) = self.invoke_virtual_view_callback(
7213 *dom_id,
7214 *node_id,
7215 bounds,
7216 window_state,
7217 renderer_resources,
7218 system_callbacks,
7219 &mut None,
7220 ) {
7221 updated_vviews.push((*dom_id, *node_id));
7222 }
7223 }
7224 }
7225
7226 updated_vviews
7227 }
7228
7229 pub fn queue_virtual_view_updates(
7233 &mut self,
7234 vviews_to_update: BTreeMap<DomId, FastBTreeSet<NodeId>>,
7235 ) {
7236 for (dom_id, node_ids) in vviews_to_update {
7237 self.pending_virtual_view_updates
7238 .entry(dom_id)
7239 .or_insert_with(FastBTreeSet::new)
7240 .extend(node_ids);
7241 }
7242 }
7243
7244 pub fn process_pending_virtual_view_updates(
7248 &mut self,
7249 window_state: &FullWindowState,
7250 renderer_resources: &RendererResources,
7251 system_callbacks: &ExternalSystemCallbacks,
7252 ) -> Vec<(DomId, NodeId)> {
7253 if self.pending_virtual_view_updates.is_empty() {
7254 return Vec::new();
7255 }
7256
7257 let vviews_to_update = core::mem::take(&mut self.pending_virtual_view_updates);
7259
7260 self.process_virtual_view_updates(
7262 &vviews_to_update,
7263 window_state,
7264 renderer_resources,
7265 system_callbacks,
7266 )
7267 }
7268
7269 fn get_virtual_view_bounds_from_layout(
7273 layout_results: &BTreeMap<DomId, DomLayoutResult>,
7274 dom_id: DomId,
7275 node_id: NodeId,
7276 ) -> Option<LogicalRect> {
7277 let layout_result = layout_results.get(&dom_id)?;
7278
7279 let node_data_container = layout_result.styled_dom.node_data.as_container();
7281 let node_data = node_data_container.get(node_id)?;
7282
7283 if !matches!(node_data.get_node_type(), NodeType::VirtualView) {
7284 return None;
7285 }
7286
7287 let layout_indices = layout_result.layout_tree.dom_to_layout.get(&node_id)?;
7289 if layout_indices.is_empty() {
7290 return None;
7291 }
7292
7293 let layout_index = layout_indices[0];
7294
7295 let position = *layout_result.calculated_positions.get(layout_index)?;
7297
7298 let layout_node = layout_result.layout_tree.get(layout_index)?;
7300 let size = layout_node.used_size?;
7301
7302 Some(LogicalRect::new(
7303 position,
7304 LogicalSize::new(size.width as f32, size.height as f32),
7305 ))
7306 }
7307}
7308
7309#[cfg(feature = "a11y")]
7310#[derive(Debug, Clone)]
7311pub enum TextEditType {
7312 ReplaceSelection(String),
7313 SetValue(String),
7314 SetNumericValue(f64),
7315}