Skip to main content

bliss_dom/
document.rs

1use crate::events::{DragMode, ScrollAnimationState, handle_dom_event};
2use crate::font_metrics::BlissFontMetricsProvider;
3use crate::layout::construct::ConstructionTask;
4use crate::layout::damage::ALL_DAMAGE;
5use crate::mutator::ViewportMut;
6use crate::net::{
7    Resource, ResourceHandler, ResourceLoadResponse, StylesheetHandler, StylesheetLoader,
8};
9use crate::node::{ImageData, NodeFlags, RasterImageData, SpecialElementData, Status, TextBrush};
10use crate::selection::TextSelection;
11use crate::stylo_to_cursor_icon::stylo_to_cursor_icon;
12use crate::traversal::TreeTraverser;
13use crate::url::DocumentUrl;
14use crate::util::ImageType;
15use crate::{
16    DEFAULT_CSS, DocumentConfig, DocumentMutator, DummyHtmlParserProvider, ElementData,
17    EventDriver, HtmlParserProvider, Node, NodeData, NoopEventHandler, TextNodeData,
18};
19use bliss_traits::devtools::DevtoolSettings;
20use bliss_traits::events::{
21    BlissScrollEvent, DomEvent, DomEventData, EventSink, HitResult, UiEvent,
22};
23use bliss_traits::navigation::{DummyNavigationProvider, NavigationProvider};
24use bliss_traits::net::{DummyNetProvider, NetProvider, Request};
25use bliss_traits::shell::{ColorScheme, DummyShellProvider, ShellProvider, Viewport};
26use cursor_icon::CursorIcon;
27use linebender_resource_handle::Blob;
28use markup5ever::local_name;
29use parley::{FontContext, PlainEditorDriver};
30use selectors::{Element, matching::QuirksMode};
31use slab::Slab;
32use std::any::Any;
33use std::cell::RefCell;
34use std::collections::{BTreeMap, Bound, HashMap, HashSet};
35use std::ops::{Deref, DerefMut};
36use std::rc::Rc;
37use std::str::FromStr;
38use std::sync::atomic::{AtomicUsize, Ordering};
39use std::sync::mpsc::{Receiver, Sender, channel};
40use std::sync::{Arc, Mutex, MutexGuard, RwLockReadGuard, RwLockWriteGuard};
41use std::task::Context as TaskContext;
42use std::time::Instant;
43use style::Atom;
44use style::animation::DocumentAnimationSet;
45use style::attr::{AttrIdentifier, AttrValue};
46use style::data::{ElementData as StyloElementData, ElementStyles};
47use style::media_queries::MediaType;
48use style::properties::ComputedValues;
49use style::properties::style_structs::Font;
50use style::queries::values::PrefersColorScheme;
51use style::selector_parser::ServoElementSnapshot;
52use style::servo_arc::Arc as ServoArc;
53use style::values::GenericAtomIdent;
54use style::values::computed::Overflow;
55use style::{
56    dom::{TDocument, TNode},
57    media_queries::{Device, MediaList},
58    selector_parser::SnapshotMap,
59    shared_lock::{SharedRwLock, StylesheetGuards},
60    stylesheets::{AllowImportRules, DocumentStyleSheet, Origin, Stylesheet},
61    stylist::Stylist,
62};
63use url::Url;
64
65#[cfg(feature = "parallel-construct")]
66use thread_local::ThreadLocal;
67
68pub enum DocGuard<'a> {
69    Ref(&'a BaseDocument),
70    RefCell(std::cell::Ref<'a, BaseDocument>),
71    RwLock(RwLockReadGuard<'a, BaseDocument>),
72    Mutex(MutexGuard<'a, BaseDocument>),
73}
74
75impl Deref for DocGuard<'_> {
76    type Target = BaseDocument;
77    #[inline(always)]
78    fn deref(&self) -> &Self::Target {
79        match self {
80            Self::Ref(base_document) => base_document,
81            Self::RefCell(refcell_guard) => refcell_guard,
82            Self::RwLock(rw_lock_read_guard) => rw_lock_read_guard,
83            Self::Mutex(mutex_guard) => mutex_guard,
84        }
85    }
86}
87
88pub enum DocGuardMut<'a> {
89    Ref(&'a mut BaseDocument),
90    RefCell(std::cell::RefMut<'a, BaseDocument>),
91    RwLock(RwLockWriteGuard<'a, BaseDocument>),
92    Mutex(MutexGuard<'a, BaseDocument>),
93}
94
95impl Deref for DocGuardMut<'_> {
96    type Target = BaseDocument;
97    #[inline(always)]
98    fn deref(&self) -> &Self::Target {
99        match self {
100            Self::Ref(base_document) => base_document,
101            Self::RefCell(refcell_guard) => refcell_guard,
102            Self::RwLock(rw_lock_read_guard) => rw_lock_read_guard,
103            Self::Mutex(mutex_guard) => mutex_guard,
104        }
105    }
106}
107
108impl DerefMut for DocGuardMut<'_> {
109    #[inline(always)]
110    fn deref_mut(&mut self) -> &mut Self::Target {
111        match self {
112            Self::Ref(base_document) => base_document,
113            Self::RefCell(refcell_guard) => &mut *refcell_guard,
114            Self::RwLock(rw_lock_read_guard) => &mut *rw_lock_read_guard,
115            Self::Mutex(mutex_guard) => &mut *mutex_guard,
116        }
117    }
118}
119
120/// Abstraction over wrappers around [`BaseDocument`] to allow for them all to
121/// be driven by [`bliss-shell`](https://docs.rs/bliss-shell)
122pub trait Document: Any + 'static {
123    fn inner(&self) -> DocGuard<'_>;
124    fn inner_mut(&mut self) -> DocGuardMut<'_>;
125
126    /// Update the [`Document`] in response to a [`UiEvent`] (click, keypress, etc)
127    fn handle_ui_event(&mut self, event: UiEvent) {
128        let mut doc = self.inner_mut();
129        let sink = doc.event_sink.clone();
130        // Only forward events to sink if events are enabled (lifecycle check)
131        // This prevents "Loading" or "Suspended" capsules from receiving events
132        match &sink {
133            Some(s) if doc.events_enabled => {
134                let mut driver =
135                    EventDriver::with_event_sink(&mut *doc, NoopEventHandler, s.as_ref());
136                driver.handle_ui_event(event);
137            }
138            _ => {
139                let mut driver = EventDriver::new(&mut *doc, NoopEventHandler);
140                driver.handle_ui_event(event);
141            }
142        }
143    }
144
145    /// Poll any pending async operations, and flush changes to the underlying [`BaseDocument`]
146    fn poll(&mut self, task_context: Option<TaskContext>) -> bool {
147        // Default implementation does nothing
148        let _ = task_context;
149        false
150    }
151
152    /// Get the [`Document`]'s id
153    fn id(&self) -> usize {
154        self.inner().id
155    }
156}
157
158pub struct PlainDocument(pub BaseDocument);
159impl Document for PlainDocument {
160    fn inner(&self) -> DocGuard<'_> {
161        DocGuard::Ref(&self.0)
162    }
163    fn inner_mut(&mut self) -> DocGuardMut<'_> {
164        DocGuardMut::Ref(&mut self.0)
165    }
166}
167
168impl Document for BaseDocument {
169    fn inner(&self) -> DocGuard<'_> {
170        DocGuard::Ref(self)
171    }
172    fn inner_mut(&mut self) -> DocGuardMut<'_> {
173        DocGuardMut::Ref(self)
174    }
175}
176
177impl Document for Rc<RefCell<BaseDocument>> {
178    fn inner(&self) -> DocGuard<'_> {
179        DocGuard::RefCell(self.borrow())
180    }
181
182    fn inner_mut(&mut self) -> DocGuardMut<'_> {
183        DocGuardMut::RefCell(self.borrow_mut())
184    }
185}
186
187pub enum DocumentEvent {
188    ResourceLoad(ResourceLoadResponse),
189}
190
191pub struct BaseDocument {
192    /// ID of the document
193    id: usize,
194
195    // Config
196    /// Base url for resolving linked resources (stylesheets, images, fonts, etc)
197    pub(crate) url: DocumentUrl,
198    // Devtool settings. Currently used to render debug overlays
199    pub(crate) devtool_settings: DevtoolSettings,
200    // Viewport details such as the dimensions, HiDPI scale, and zoom factor,
201    pub(crate) viewport: Viewport,
202    // Scroll within our viewport
203    pub(crate) viewport_scroll: crate::Point<f64>,
204
205    // Events
206    pub(crate) tx: Sender<DocumentEvent>,
207    // rx will always be Some, except temporarily while processing events
208    pub(crate) rx: Option<Receiver<DocumentEvent>>,
209
210    /// A slab-backed tree of nodes
211    ///
212    /// We pin the tree to a guarantee to the nodes it creates that the tree is stable in memory.
213    /// There is no way to create the tree - publicly or privately - that would invalidate that invariant.
214    pub(crate) nodes: Box<Slab<Node>>,
215
216    // Stylo
217    /// The Stylo engine
218    pub(crate) stylist: Stylist,
219    pub(crate) animations: DocumentAnimationSet,
220    /// Stylo shared lock
221    pub(crate) guard: SharedRwLock,
222    /// Stylo invalidation map. We insert into this map prior to mutating nodes.
223    pub(crate) snapshots: SnapshotMap,
224
225    // Parley contexts
226    /// A Parley font context
227    pub(crate) font_ctx: Arc<Mutex<parley::FontContext>>,
228    #[cfg(feature = "parallel-construct")]
229    /// Thread-and-document-local copies to the font context
230    pub(crate) thread_font_contexts: ThreadLocal<RefCell<Box<FontContext>>>,
231    /// A Parley layout context
232    pub(crate) layout_ctx: parley::LayoutContext<TextBrush>,
233
234    /// The node which is currently hovered (if any)
235    pub(crate) hover_node_id: Option<usize>,
236    /// Whether the node which is currently hovered is a text node/span
237    pub(crate) hover_node_is_text: bool,
238    /// The node which is currently focussed (if any)
239    pub(crate) focus_node_id: Option<usize>,
240    /// The node which is currently active (if any)
241    pub(crate) active_node_id: Option<usize>,
242    /// The node which received a mousedown event (if any)
243    pub(crate) mousedown_node_id: Option<usize>,
244    /// The last time a mousedown was made (for double-click detection)
245    pub(crate) last_mousedown_time: Option<Instant>,
246    /// The position where mousedown occurred (for selection drags and double-click detection)
247    pub(crate) mousedown_position: taffy::Point<f32>,
248    /// How many clicks have been made in quick succession
249    pub(crate) click_count: u16,
250    /// Whether we're currently in a text selection drag (moved 2px+ from mousedown)
251    pub(crate) drag_mode: DragMode,
252    /// Whether and what kind of scroll animation is currently in progress
253    pub(crate) scroll_animation: ScrollAnimationState,
254
255    /// Text selection state (for non-input text)
256    pub(crate) text_selection: TextSelection,
257
258    // TODO: collapse animating state into a bitflags
259    /// Whether there are active CSS animations/transitions (so we should re-render every frame)
260    pub(crate) has_active_animations: bool,
261    /// Whether there is a `<canvas>` element in the DOM (so we should re-render every frame)
262    pub(crate) has_canvas: bool,
263    /// Whether there are subdocuments that are animating (so we should re-render every frame)
264    pub(crate) subdoc_is_animating: bool,
265
266    /// Map of node ID's for fast lookups
267    pub(crate) nodes_to_id: HashMap<String, usize>,
268    /// Map of `<style>` and `<link>` node IDs to their associated stylesheet
269    pub(crate) nodes_to_stylesheet: BTreeMap<usize, DocumentStyleSheet>,
270    /// Stylesheets added by the useragent
271    /// where the key is the hashed CSS
272    pub(crate) ua_stylesheets: HashMap<String, DocumentStyleSheet>,
273    /// Map from form control node ID's to their associated forms node ID's
274    pub(crate) controls_to_form: HashMap<usize, usize>,
275    /// Nodes that contain sub documents
276    pub(crate) sub_document_nodes: HashSet<usize>,
277    /// Set of changed nodes for updating the accessibility tree
278    pub(crate) changed_nodes: HashSet<usize>,
279    /// Set of changed nodes for updating the accessibility tree
280    pub(crate) deferred_construction_nodes: Vec<ConstructionTask>,
281
282    /// Cache of loaded images, keyed by URL. Allows reusing images across multiple
283    /// elements without re-fetching from the network.
284    pub(crate) image_cache: HashMap<String, ImageData>,
285
286    /// Tracks in-flight image requests. When an image is being fetched, additional
287    /// requests for the same URL are queued here instead of starting new fetches.
288    /// Value is a list of (node_id, image_type) pairs waiting for the image.
289    pub(crate) pending_images: HashMap<String, Vec<(usize, ImageType)>>,
290
291    /// Event listener registry, mapping (node_id, event_name) to registered handler_ids.
292    /// Used by addEventListener/removeEventListener via the DomController API.
293    /// The handler_id is an opaque identifier that the caller (e.g. a script engine)
294    /// uses to dispatch callbacks when the event fires.
295    pub(crate) event_listeners: HashMap<(usize, String), Vec<u64>>,
296
297    /// Scoped stylesheets for each shadow root, keyed by shadow root node ID.
298    /// When a `<style>` element is inside a shadow root, its stylesheet is stored
299    /// here instead of being added to the global stylist. These are used to build
300    /// per-shadow-root CascadeData during style resolution.
301    pub(crate) shadow_scoped_sheets: HashMap<usize, Vec<DocumentStyleSheet>>,
302
303    /// Pre-built CascadeData for each shadow root, keyed by shadow root node ID.
304    /// Rebuilt during `resolve_stylist()` from `shadow_scoped_sheets`. Accessed by
305    /// `TShadowRoot::style_data()` to apply scoped styles within shadow roots.
306    pub(crate) shadow_cascade_data: HashMap<usize, Box<style::stylist::CascadeData>>,
307
308    // Service providers
309    /// Network provider. Can be used to fetch assets.
310    pub net_provider: Arc<dyn NetProvider>,
311    /// Navigation provider. Can be used to navigate to a new page (bubbles up the event
312    /// on e.g. clicking a Link)
313    pub navigation_provider: Arc<dyn NavigationProvider>,
314    /// Shell provider. Can be used to request a redraw or set the cursor icon
315    pub shell_provider: Arc<dyn ShellProvider>,
316    /// HTML parser provider. Used to parse HTML for setInnerHTML
317    pub html_parser_provider: Arc<dyn HtmlParserProvider>,
318
319    /// Script engine for executing JavaScript, Lua, Python, etc.
320    pub(crate) script_engine: Option<crate::script::BoxedScriptEngine>,
321
322    /// Event sink for external event observation
323    pub(crate) event_sink: Option<Arc<dyn EventSink>>,
324
325    /// Whether events should be forwarded to the event sink
326    /// Used for lifecycle management - only "Running" capsules should receive events
327    pub(crate) events_enabled: bool,
328}
329
330pub(crate) fn make_device(viewport: &Viewport, font_ctx: Arc<Mutex<FontContext>>) -> Device {
331    let width = viewport.window_size.0 as f32 / viewport.scale();
332    let height = viewport.window_size.1 as f32 / viewport.scale();
333    let viewport_size = euclid::Size2D::new(width, height);
334    let device_pixel_ratio = euclid::Scale::new(viewport.scale());
335
336    Device::new(
337        MediaType::screen(),
338        selectors::matching::QuirksMode::NoQuirks,
339        viewport_size,
340        device_pixel_ratio,
341        Box::new(BlissFontMetricsProvider { font_ctx }),
342        ComputedValues::initial_values_with_font_override(Font::initial_values()),
343        match viewport.color_scheme {
344            ColorScheme::Light => PrefersColorScheme::Light,
345            ColorScheme::Dark => PrefersColorScheme::Dark,
346        },
347    )
348}
349
350impl BaseDocument {
351    /// Create a new (empty) [`BaseDocument`] with the specified configuration
352    pub fn new(config: DocumentConfig) -> Self {
353        static ID_GENERATOR: AtomicUsize = AtomicUsize::new(1);
354
355        let id = ID_GENERATOR.fetch_add(1, Ordering::SeqCst);
356
357        let font_ctx = config
358            .font_ctx
359            // .map(|mut font_ctx| {
360            //     font_ctx.collection.make_shared();
361            //     font_ctx.source_cache.make_shared();
362            //     font_ctx
363            // })
364            .unwrap_or_else(|| {
365                // let mut font_ctx = FontContext {
366                //     source_cache: SourceCache::new_shared(),
367                //     collection: Collection::new(CollectionOptions {
368                //         shared: true,
369                //         system_fonts: true,
370                //     }),
371                // };
372                let mut font_ctx = FontContext::default();
373                font_ctx
374                    .collection
375                    .register_fonts(Blob::new(Arc::new(crate::BULLET_FONT) as _), None);
376                font_ctx
377            });
378        let font_ctx = Arc::new(Mutex::new(font_ctx));
379
380        let viewport = config.viewport.unwrap_or_default();
381        let device = make_device(&viewport, font_ctx.clone());
382        let stylist = Stylist::new(device, QuirksMode::NoQuirks);
383        let snapshots = SnapshotMap::new();
384        let nodes = Box::new(Slab::new());
385        let guard = SharedRwLock::new();
386        let nodes_to_id = HashMap::new();
387
388        // Make sure we turn on stylo features
389        style_config::set_bool("layout.flexbox.enabled", true);
390        style_config::set_bool("layout.grid.enabled", true);
391        style_config::set_bool("layout.legacy_layout", true);
392        style_config::set_bool("layout.unimplemented", true);
393        style_config::set_bool("layout.columns.enabled", true);
394
395        let base_url = config
396            .base_url
397            .and_then(|url| DocumentUrl::from_str(&url).ok())
398            .unwrap_or_default();
399
400        let net_provider = config
401            .net_provider
402            .unwrap_or_else(|| Arc::new(DummyNetProvider));
403        let navigation_provider = config
404            .navigation_provider
405            .unwrap_or_else(|| Arc::new(DummyNavigationProvider));
406        let shell_provider = config
407            .shell_provider
408            .unwrap_or_else(|| Arc::new(DummyShellProvider));
409        let html_parser_provider = config
410            .html_parser_provider
411            .unwrap_or_else(|| Arc::new(DummyHtmlParserProvider));
412
413        let (tx, rx) = channel();
414
415        let mut doc = Self {
416            id,
417            tx,
418            rx: Some(rx),
419
420            guard,
421            nodes,
422            stylist,
423            animations: DocumentAnimationSet::default(),
424            snapshots,
425            nodes_to_id,
426            viewport,
427            devtool_settings: DevtoolSettings::default(),
428            viewport_scroll: crate::Point::ZERO,
429            url: base_url,
430            ua_stylesheets: HashMap::new(),
431            nodes_to_stylesheet: BTreeMap::new(),
432            font_ctx,
433            #[cfg(feature = "parallel-construct")]
434            thread_font_contexts: ThreadLocal::new(),
435            layout_ctx: parley::LayoutContext::new(),
436
437            hover_node_id: None,
438            hover_node_is_text: false,
439            focus_node_id: None,
440            active_node_id: None,
441            mousedown_node_id: None,
442            has_active_animations: false,
443            subdoc_is_animating: false,
444            has_canvas: false,
445            sub_document_nodes: HashSet::new(),
446            changed_nodes: HashSet::new(),
447            deferred_construction_nodes: Vec::new(),
448            image_cache: HashMap::new(),
449            pending_images: HashMap::new(),
450            event_listeners: HashMap::new(),
451            shadow_scoped_sheets: HashMap::new(),
452            shadow_cascade_data: HashMap::new(),
453            controls_to_form: HashMap::new(),
454            net_provider,
455            navigation_provider,
456            shell_provider,
457            html_parser_provider,
458            script_engine: None,
459            event_sink: None,
460            events_enabled: false,
461            last_mousedown_time: None,
462            mousedown_position: taffy::Point::ZERO,
463            click_count: 0,
464            drag_mode: DragMode::None,
465            scroll_animation: ScrollAnimationState::None,
466            text_selection: TextSelection::default(),
467        };
468
469        // Initialise document with root Document node
470        doc.create_node(NodeData::Document);
471        doc.root_node_mut().flags.insert(NodeFlags::IS_IN_DOCUMENT);
472
473        match config.ua_stylesheets {
474            Some(stylesheets) => {
475                for ss in &stylesheets {
476                    doc.add_user_agent_stylesheet(ss);
477                }
478            }
479            None => doc.add_user_agent_stylesheet(DEFAULT_CSS),
480        }
481
482        // Stylo data on the root node container is needed to render the node
483        let stylo_element_data = StyloElementData {
484            styles: ElementStyles {
485                primary: Some(
486                    ComputedValues::initial_values_with_font_override(Font::initial_values())
487                        .to_arc(),
488                ),
489                ..Default::default()
490            },
491            ..Default::default()
492        };
493        *doc.root_node().stylo_element_data.borrow_mut() = Some(stylo_element_data);
494
495        doc
496    }
497
498    /// Set the Document's networking provider
499    pub fn set_net_provider(&mut self, net_provider: Arc<dyn NetProvider>) {
500        self.net_provider = net_provider;
501    }
502
503    /// Set the Document's navigation provider
504    pub fn set_navigation_provider(&mut self, navigation_provider: Arc<dyn NavigationProvider>) {
505        self.navigation_provider = navigation_provider;
506    }
507
508    /// Set the Document's shell provider
509    pub fn set_shell_provider(&mut self, shell_provider: Arc<dyn ShellProvider>) {
510        self.shell_provider = shell_provider;
511    }
512
513    /// Set the Document's html parser provider
514    pub fn set_html_parser_provider(&mut self, html_parser_provider: Arc<dyn HtmlParserProvider>) {
515        self.html_parser_provider = html_parser_provider;
516    }
517
518    /// Set the script engine for this document
519    pub fn set_script_engine(&mut self, mut engine: crate::script::BoxedScriptEngine) {
520        engine.init(self);
521        self.script_engine = Some(engine);
522    }
523
524    /// Set the event sink for this document
525    pub fn set_event_sink(&mut self, sink: Arc<dyn EventSink>) {
526        self.event_sink = Some(sink);
527    }
528
529    /// Enable or disable event forwarding to the event sink
530    /// This is used for lifecycle management - events should only be forwarded when the capsule is "Running"
531    pub fn set_events_enabled(&mut self, enabled: bool) {
532        self.events_enabled = enabled;
533    }
534
535    /// Query registered event listeners for a given node ID and event name.
536    /// Returns an empty Vec if no listeners are registered.
537    pub fn get_event_listeners(&self, node_id: usize, event_name: &str) -> &[u64] {
538        self.event_listeners
539            .get(&(node_id, event_name.to_string()))
540            .map(|v| v.as_slice())
541            .unwrap_or(&[])
542    }
543
544    /// Execute script code
545    pub fn execute_script(
546        &mut self,
547        code: &str,
548        language: crate::script::ScriptLanguage,
549        context: &crate::script::ExecutionContext,
550    ) -> Result<crate::script::ScriptValue, crate::script::ScriptError> {
551        if let Some(engine) = &mut self.script_engine {
552            engine.execute(code, language, context)
553        } else {
554            Err(crate::script::ScriptError::UnsupportedLanguage(format!(
555                "{:?}",
556                language
557            )))
558        }
559    }
560
561    /// Poll script engine for async work
562    pub fn poll_script_engine(&mut self) -> Result<bool, crate::script::ScriptError> {
563        if let Some(engine) = &mut self.script_engine {
564            engine.tick()
565        } else {
566            Ok(false)
567        }
568    }
569
570    /// Set error handler for script engine
571    pub fn set_script_error_handler(
572        &mut self,
573        callback: Option<crate::script::ScriptErrorCallback>,
574    ) {
575        if let Some(engine) = &mut self.script_engine {
576            engine.set_error_handler(callback);
577        }
578    }
579
580    /// Handle UI event with script engine support
581    pub fn handle_ui_event_with_script(&mut self, event: &DomEvent) -> crate::script::EventHandled {
582        if let Some(engine) = &mut self.script_engine {
583            engine.handle_event(event)
584        } else {
585            crate::script::EventHandled::Propagate
586        }
587    }
588
589    /// Set base url for resolving linked resources (stylesheets, images, fonts, etc)
590    pub fn set_base_url(&mut self, url: &str) {
591        self.url = DocumentUrl::from(Url::parse(url).unwrap_or_else(|_| {
592            Url::parse("about:blank").unwrap()
593        }));
594    }
595
596    pub fn guard(&self) -> &SharedRwLock {
597        &self.guard
598    }
599
600    pub fn tree(&self) -> &Slab<Node> {
601        &self.nodes
602    }
603
604    pub fn id(&self) -> usize {
605        self.id
606    }
607
608    pub fn get_node(&self, node_id: usize) -> Option<&Node> {
609        self.nodes.get(node_id)
610    }
611
612    pub fn get_node_mut(&mut self, node_id: usize) -> Option<&mut Node> {
613        self.nodes.get_mut(node_id)
614    }
615
616    pub fn get_focussed_node_id(&self) -> Option<usize> {
617        self.focus_node_id
618            .or(self.try_root_element().map(|el| el.id))
619    }
620
621    pub fn mutate<'doc>(&'doc mut self) -> DocumentMutator<'doc> {
622        DocumentMutator::new(self)
623    }
624
625    pub fn handle_dom_event<F: FnMut(DomEvent)>(
626        &mut self,
627        event: &mut DomEvent,
628        dispatch_event: F,
629    ) {
630        handle_dom_event(self, event, dispatch_event)
631    }
632
633    pub fn as_any_mut(&mut self) -> &mut dyn Any {
634        self
635    }
636
637    /// Find the label's bound input elements:
638    /// the element id referenced by the "for" attribute of a given label element
639    /// or the first input element which is nested in the label
640    /// Note that although there should only be one bound element,
641    /// we return all possibilities instead of just the first
642    /// in order to allow the caller to decide which one is correct
643    pub fn label_bound_input_element(&self, label_node_id: usize) -> Option<&Node> {
644        let label_element = self.get_node(label_node_id)?.element_data()?;
645        if let Some(target_element_dom_id) = label_element.attr(local_name!("for")) {
646            TreeTraverser::new(self)
647                .filter_map(|id| {
648                    let node = self.get_node(id)?;
649                    let element_data = node.element_data()?;
650                    if element_data.name.local != local_name!("input") {
651                        return None;
652                    }
653                    let id = element_data.id.as_ref()?;
654                    if *id == *target_element_dom_id {
655                        Some(node)
656                    } else {
657                        None
658                    }
659                })
660                .next()
661        } else {
662            TreeTraverser::new_with_root(self, label_node_id)
663                .filter_map(|child_id| {
664                    let node = self.get_node(child_id)?;
665                    let element_data = node.element_data()?;
666                    if element_data.name.local == local_name!("input") {
667                        Some(node)
668                    } else {
669                        None
670                    }
671                })
672                .next()
673        }
674    }
675
676    pub fn toggle_checkbox(el: &mut ElementData) -> bool {
677        let Some(is_checked) = el.checkbox_input_checked_mut() else {
678            return false;
679        };
680        *is_checked = !*is_checked;
681
682        *is_checked
683    }
684
685    pub fn toggle_radio(&mut self, radio_set_name: String, target_radio_id: usize) {
686        for (i, node) in self.nodes.iter_mut() {
687            if let Some(node_data) = node.data.downcast_element_mut() {
688                if node_data.attr(local_name!("name")) == Some(&radio_set_name) {
689                    let was_clicked = i == target_radio_id;
690                    let Some(is_checked) = node_data.checkbox_input_checked_mut() else {
691                        continue;
692                    };
693                    *is_checked = was_clicked;
694                }
695            }
696        }
697    }
698
699    pub fn set_style_property(&mut self, node_id: usize, name: &str, value: &str) {
700        let Some(node) = self.nodes.get_mut(node_id) else {
701            return;
702        };
703        // Non-element nodes have no element data to write style onto; silently
704        // ignore instead of panicking on attacker-controlled HTML.
705        let Some(el) = node.element_data_mut() else {
706            return;
707        };
708        el.set_style_property(name, value, &self.guard, self.url.url_extra_data());
709    }
710
711    pub fn remove_style_property(&mut self, node_id: usize, name: &str) {
712        let Some(node) = self.nodes.get_mut(node_id) else {
713            return;
714        };
715        let Some(el) = node.element_data_mut() else {
716            return;
717        };
718        el.remove_style_property(name, &self.guard, self.url.url_extra_data());
719    }
720
721    pub fn set_sub_document(&mut self, node_id: usize, sub_document: Box<dyn Document>) {
722        let Some(node) = self.get_node_mut(node_id) else {
723            return;
724        };
725        let Some(el) = node.element_data_mut() else {
726            return;
727        };
728        el.set_sub_document(sub_document);
729        self.sub_document_nodes.insert(node_id);
730    }
731
732    pub fn remove_sub_document(&mut self, node_id: usize) {
733        let Some(node) = self.get_node_mut(node_id) else {
734            self.sub_document_nodes.remove(&node_id);
735            return;
736        };
737        let Some(el) = node.element_data_mut() else {
738            self.sub_document_nodes.remove(&node_id);
739            return;
740        };
741        el.remove_sub_document();
742        self.sub_document_nodes.remove(&node_id);
743    }
744
745    pub fn root_node(&self) -> &Node {
746        &self.nodes[0]
747    }
748
749    pub fn root_node_mut(&mut self) -> &mut Node {
750        &mut self.nodes[0]
751    }
752
753    pub fn root_element(&self) -> Option<&Node> {
754        TDocument::as_node(&self.root_node()).first_element_child()
755    }
756
757    /// Deprecated alias for [`BaseDocument::root_element`].
758    /// Both return `Option<&Node>` after D-2c-followup widened the signature
759    /// to eliminate the chained-unwrap panic when the document has no
760    /// element child (e.g. before any HTML is parsed into it). Retained for
761    /// callers that want explicit Option-style naming.
762    pub fn try_root_element(&self) -> Option<&Node> {
763        self.root_element()
764    }
765
766    pub fn create_node(&mut self, node_data: NodeData) -> usize {
767        // Cast to *const since Node only needs shared access to the slab via tree()
768        let slab_ptr = self.nodes.as_ref() as *const Slab<Node>;
769        let doc_ptr = self as *const BaseDocument;
770        let guard = self.guard.clone();
771
772        let entry = self.nodes.vacant_entry();
773        let id = entry.key();
774        entry.insert(Node::new(slab_ptr, doc_ptr, id, guard, node_data));
775
776        // Mark the new node as changed.
777        self.changed_nodes.insert(id);
778        id
779    }
780
781    pub(crate) fn drop_node_ignoring_parent(&mut self, node_id: usize) -> Option<Node> {
782        let mut node = self.nodes.try_remove(node_id);
783        if let Some(node) = &mut node {
784            if let Some(before) = node.before {
785                self.drop_node_ignoring_parent(before);
786            }
787            if let Some(after) = node.after {
788                self.drop_node_ignoring_parent(after);
789            }
790
791            for &child in &node.children {
792                self.drop_node_ignoring_parent(child);
793            }
794        }
795        node
796    }
797
798    /// Whether the document has been mutated
799    pub fn has_changes(&self) -> bool {
800        !self.changed_nodes.is_empty()
801    }
802
803    pub fn create_text_node(&mut self, text: &str) -> usize {
804        let content = text.to_string();
805        let data = NodeData::Text(TextNodeData::new(content));
806        self.create_node(data)
807    }
808
809    /// Hardened for D-2c: atomic recursive clone. If `node_id` is stale OR any
810    /// descendant goes stale mid-recursion, the clone rolls back: the parent
811    /// slot and every already-cloned child slot are dropped from the slab via
812    /// `drop_node_ignoring_parent`, returning cleanly without panicking.
813    ///
814    /// On any failure path, returns `usize::MAX` as a documented sentinel —
815    /// unreachable in practice (slabs don't reach that size), so it eliminates
816    /// the collision with the document root slot id (`0`) that a naïve `0`
817    /// sentinel would have. Callers — e.g. html5ever's `TreeSink::clone_subtree`
818    /// trait method — can safely pass the result on without disambiguating,
819    /// trusting that a real clone produces a slot id strictly in `[1, MAX-1]`.
820    pub fn deep_clone_node(&mut self, node_id: usize) -> usize {
821        let Some(node) = self.get_node(node_id) else {
822            debug_assert!(
823                false,
824                "deep_clone_node: stale root node_id={node_id}"
825            );
826            return usize::MAX;
827        };
828        let data = node.data.clone();
829        let children = node.children.clone();
830
831        let new_node_id = self.create_node(data);
832
833        // Atomic clone: a stale descendant aborts the whole operation and
834        // rolls back any already-cloned children + the parent slot.
835        let mut new_children = Vec::with_capacity(children.len());
836        for child_id in children {
837            let cloned = self.deep_clone_node(child_id);
838            if cloned == usize::MAX {
839                // Rollback window: drop already-cloned children + the orphan parent.
840                for &drop_id in &new_children {
841                    self.drop_node_ignoring_parent(drop_id);
842                }
843                self.drop_node_ignoring_parent(new_node_id);
844                return usize::MAX;
845            }
846            new_children.push(child_id);
847        }
848
849        // Wire parent + children. By construction every id here was just
850        // produced by `create_node` or a successful recursive call.
851        for &child_id in &new_children {
852            match self.get_node_mut(child_id) {
853                Some(child) => child.parent = Some(new_node_id),
854                None => {
855                    debug_assert!(
856                        false,
857                        "deep_clone_node: vanished child_id={child_id}"
858                    );
859                    return usize::MAX;
860                }
861            }
862        }
863        match self.get_node_mut(new_node_id) {
864            Some(new_node) => new_node.children = new_children,
865            None => {
866                debug_assert!(
867                    false,
868                    "deep_clone_node: vanished new_node_id={new_node_id}"
869                );
870                return usize::MAX;
871            }
872        }
873
874        new_node_id
875    }
876
877    pub(crate) fn remove_and_drop_pe(&mut self, node_id: usize) -> Option<Node> {
878        fn remove_pe_ignoring_parent(doc: &mut BaseDocument, node_id: usize) -> Option<Node> {
879            let mut node = doc.nodes.try_remove(node_id);
880            if let Some(node) = &mut node {
881                for &child in &node.children {
882                    remove_pe_ignoring_parent(doc, child);
883                }
884            }
885            node
886        }
887
888        let node = remove_pe_ignoring_parent(self, node_id);
889
890        // Update child_idx values
891        if let Some(parent_id) = node.as_ref().and_then(|node| node.parent) {
892            let parent = &mut self.nodes[parent_id];
893            parent.children.retain(|id| *id != node_id);
894        }
895
896        node
897    }
898
899    pub(crate) fn resolve_url(&self, raw: &str) -> Option<url::Url> {
900        let result = self.url.resolve_relative(raw);
901        #[cfg(feature = "tracing")]
902        if result.is_none() {
903            tracing::warn!("Failed to resolve URL '{raw}' with base {:?}", *self.url);
904        }
905        result
906    }
907
908    pub fn print_tree(&self) {
909        crate::util::walk_tree(0, self.root_node());
910    }
911
912    pub fn print_subtree(&self, node_id: usize) {
913        crate::util::walk_tree(0, &self.nodes[node_id]);
914    }
915
916    pub fn reload_resource_by_href(&mut self, href_to_reload: &str) {
917        for &node_id in self.nodes_to_stylesheet.keys() {
918            let Some(node) = self.get_node(node_id) else {
919                debug_assert!(
920                    false,
921                    "reload_resource_by_href: stale node_id={node_id} (still in nodes_to_stylesheet)"
922                );
923                continue;
924            };
925            let Some(element) = node.element_data() else {
926                continue;
927            };
928
929            if element.name.local == local_name!("link") {
930                if let Some(href) = element.attr(local_name!("href")) {
931                    // println!("Node {node_id} {href} {href_to_reload} {} {}", resolved_href.as_str(), resolved_href.as_str() == url_to_reload);
932                    if href == href_to_reload {
933                        let Some(resolved_href) = self.resolve_url(href) else {
934                            continue;
935                        };
936                        self.net_provider.fetch(
937                            self.id(),
938                            Request::get(resolved_href.clone()),
939                            ResourceHandler::boxed(
940                                self.tx.clone(),
941                                self.id,
942                                Some(node_id),
943                                self.shell_provider.clone(),
944                                StylesheetHandler {
945                                    source_url: resolved_href,
946                                    guard: self.guard.clone(),
947                                    net_provider: self.net_provider.clone(),
948                                },
949                            ),
950                        );
951                    }
952                }
953            }
954        }
955    }
956
957    pub fn process_style_element(&mut self, target_id: usize) {
958        let Some(node) = self.get_node(target_id) else {
959            debug_assert!(
960                false,
961                "process_style_element called with stale target_id={target_id}"
962            );
963            return;
964        };
965        let css = node.text_content();
966        let css = html_escape::decode_html_entities(&css);
967        let sheet = self.make_stylesheet(&css, Origin::Author);
968
969        // Check if this <style> element is inside a shadow root.
970        // If so, route its stylesheet to the shadow root's scoped storage
971        // instead of the global stylist, so the rules only apply within
972        // the shadow tree.
973        if let Some(shadow_root_id) = self.find_containing_shadow_root(target_id) {
974            self.shadow_scoped_sheets
975                .entry(shadow_root_id)
976                .or_default()
977                .push(sheet);
978            // Mark the cascade data as needing rebuild
979            self.shadow_cascade_data.remove(&shadow_root_id);
980            return;
981        }
982
983        self.add_stylesheet_for_node(sheet, target_id);
984    }
985
986    /// Walk up the parent chain from `node_id` to find the nearest shadow root ancestor.
987    /// Returns `Some(shadow_root_node_id)` if one is found, `None` otherwise.
988    /// Hardened for D-2c: bails with `debug_assert!` on a stale id rather than
989    /// panicking on a raw slab index.
990    fn find_containing_shadow_root(&self, node_id: usize) -> Option<usize> {
991        let Some(start) = self.get_node(node_id) else {
992            debug_assert!(
993                false,
994                "find_containing_shadow_root: stale node_id={node_id}"
995            );
996            return None;
997        };
998        let mut current = start.parent;
999        while let Some(parent_id) = current {
1000            let Some(parent) = self.get_node(parent_id) else {
1001                debug_assert!(
1002                    false,
1003                    "find_containing_shadow_root: stale parent_id={parent_id}"
1004                );
1005                return None;
1006            };
1007            if matches!(parent.data, NodeData::ShadowRoot { .. }) {
1008                return Some(parent_id);
1009            }
1010            current = parent.parent;
1011        }
1012        None
1013    }
1014
1015    pub fn remove_user_agent_stylesheet(&mut self, contents: &str) {
1016        if let Some(sheet) = self.ua_stylesheets.remove(contents) {
1017            self.stylist.remove_stylesheet(sheet, &self.guard.read());
1018        }
1019    }
1020
1021    pub fn add_user_agent_stylesheet(&mut self, css: &str) {
1022        let sheet = self.make_stylesheet(css, Origin::UserAgent);
1023        self.ua_stylesheets.insert(css.to_string(), sheet.clone());
1024        self.stylist.append_stylesheet(sheet, &self.guard.read());
1025    }
1026
1027    pub fn make_stylesheet(&self, css: impl AsRef<str>, origin: Origin) -> DocumentStyleSheet {
1028        let data = Stylesheet::from_str(
1029            css.as_ref(),
1030            self.url.url_extra_data(),
1031            origin,
1032            ServoArc::new(self.guard.wrap(MediaList::empty())),
1033            self.guard.clone(),
1034            Some(&StylesheetLoader {
1035                tx: self.tx.clone(),
1036                doc_id: self.id,
1037                net_provider: self.net_provider.clone(),
1038                shell_provider: self.shell_provider.clone(),
1039            }),
1040            None,
1041            QuirksMode::NoQuirks,
1042            AllowImportRules::Yes,
1043        );
1044
1045        DocumentStyleSheet(ServoArc::new(data))
1046    }
1047
1048    pub fn upsert_stylesheet_for_node(&mut self, node_id: usize) {
1049        let Some(node) = self.get_node(node_id) else {
1050            return;
1051        };
1052        let raw_styles = node.text_content();
1053        let sheet = self.make_stylesheet(raw_styles, Origin::Author);
1054        self.add_stylesheet_for_node(sheet, node_id);
1055    }
1056
1057    pub fn add_stylesheet_for_node(&mut self, stylesheet: DocumentStyleSheet, node_id: usize) {
1058        let old = self.nodes_to_stylesheet.insert(node_id, stylesheet.clone());
1059
1060        if let Some(old) = old {
1061            self.stylist.remove_stylesheet(old, &self.guard.read())
1062        }
1063
1064        // Fetch @font-face fonts
1065        crate::net::fetch_font_face(
1066            self.tx.clone(),
1067            self.id,
1068            Some(node_id),
1069            &stylesheet.0,
1070            &self.net_provider,
1071            &self.shell_provider,
1072            &self.guard.read(),
1073        );
1074
1075        // Store data on element
1076        let Some(node) = self.get_node_mut(node_id) else {
1077            return;
1078        };
1079        let Some(element) = node.element_data_mut() else {
1080            debug_assert!(
1081                false,
1082                "add_stylesheet_for_node: node_id={node_id} is not an element"
1083            );
1084            return;
1085        };
1086        element.special_data = SpecialElementData::Stylesheet(stylesheet.clone());
1087
1088        // TODO: Nodes could potentially get reused so ordering by node_id might be wrong.
1089        let insertion_point = self
1090            .nodes_to_stylesheet
1091            .range((Bound::Excluded(node_id), Bound::Unbounded))
1092            .next()
1093            .map(|(_, sheet)| sheet);
1094
1095        if let Some(insertion_point) = insertion_point {
1096            self.stylist.insert_stylesheet_before(
1097                stylesheet,
1098                insertion_point.clone(),
1099                &self.guard.read(),
1100            )
1101        } else {
1102            self.stylist
1103                .append_stylesheet(stylesheet, &self.guard.read())
1104        }
1105    }
1106
1107    pub fn handle_messages(&mut self) {
1108        // Remove event Receiver from the Document so that we can process events
1109        // without holding a borrow to the Document
1110        let rx = self.rx.take().unwrap();
1111
1112        while let Ok(msg) = rx.try_recv() {
1113            self.handle_message(msg);
1114        }
1115
1116        // Put Receiver back
1117        self.rx = Some(rx);
1118    }
1119
1120    pub fn handle_message(&mut self, msg: DocumentEvent) {
1121        match msg {
1122            DocumentEvent::ResourceLoad(resource) => self.load_resource(resource),
1123        }
1124    }
1125
1126    pub fn load_resource(&mut self, res: ResourceLoadResponse) {
1127        let Ok(resource) = res.result else {
1128            // TODO: handle error
1129            return;
1130        };
1131
1132        match resource {
1133            Resource::Css(css) => {
1134                let node_id = res.node_id.unwrap();
1135                self.add_stylesheet_for_node(css, node_id);
1136            }
1137            Resource::Image(_kind, width, height, image_data) => {
1138                // Create the ImageData and cache it
1139                let image = ImageData::Raster(RasterImageData::new(width, height, image_data));
1140
1141                let Some(url) = res.resolved_url.as_ref() else {
1142                    return;
1143                };
1144
1145                // Get all nodes waiting for this image
1146                let waiting_nodes = self.pending_images.remove(url).unwrap_or_default();
1147
1148                #[cfg(feature = "tracing")]
1149                tracing::info!(
1150                    "Image {url} loaded, applying to {} nodes",
1151                    waiting_nodes.len()
1152                );
1153
1154                // Cache the image
1155                self.image_cache.insert(url.clone(), image.clone());
1156
1157                // Apply to all waiting nodes
1158                for (node_id, image_type) in waiting_nodes {
1159                    let Some(node) = self.get_node_mut(node_id) else {
1160                        continue;
1161                    };
1162
1163                    match image_type {
1164                        ImageType::Image => {
1165                            node.element_data_mut().unwrap().special_data =
1166                                SpecialElementData::Image(Box::new(image.clone()));
1167
1168                            // Clear layout cache
1169                            node.cache.clear();
1170                            node.insert_damage(ALL_DAMAGE);
1171                        }
1172                        ImageType::Background(idx) => {
1173                            if let Some(Some(bg_image)) = node
1174                                .element_data_mut()
1175                                .and_then(|el| el.background_images.get_mut(idx))
1176                            {
1177                                bg_image.status = Status::Ok;
1178                                bg_image.image = image.clone();
1179                            }
1180                        }
1181                    }
1182                }
1183            }
1184            #[cfg(feature = "svg")]
1185            Resource::Svg(_kind, tree) => {
1186                // Create the ImageData and cache it
1187                let image = ImageData::Svg(tree);
1188
1189                let Some(url) = res.resolved_url.as_ref() else {
1190                    return;
1191                };
1192
1193                // Get all nodes waiting for this image
1194                let waiting_nodes = self.pending_images.remove(url).unwrap_or_default();
1195
1196                #[cfg(feature = "tracing")]
1197                tracing::info!(
1198                    "SVG {url} loaded, applying to {} nodes",
1199                    waiting_nodes.len()
1200                );
1201
1202                // Cache the image
1203                self.image_cache.insert(url.clone(), image.clone());
1204
1205                // Apply to all waiting nodes
1206                for (node_id, image_type) in waiting_nodes {
1207                    let Some(node) = self.get_node_mut(node_id) else {
1208                        continue;
1209                    };
1210
1211                    match image_type {
1212                        ImageType::Image => {
1213                            node.element_data_mut().unwrap().special_data =
1214                                SpecialElementData::Image(Box::new(image.clone()));
1215
1216                            // Clear layout cache
1217                            node.cache.clear();
1218                            node.insert_damage(ALL_DAMAGE);
1219                        }
1220                        ImageType::Background(idx) => {
1221                            if let Some(Some(bg_image)) = node
1222                                .element_data_mut()
1223                                .and_then(|el| el.background_images.get_mut(idx))
1224                            {
1225                                bg_image.status = Status::Ok;
1226                                bg_image.image = image.clone();
1227                            }
1228                        }
1229                    }
1230                }
1231            }
1232            Resource::Font(bytes) => {
1233                let font = Blob::new(Arc::new(bytes));
1234
1235                // TODO: Implement FontInfoOveride
1236                // TODO: Investigate eliminating double-box
1237                let mut global_font_ctx = self.font_ctx.lock().unwrap_or_else(|e| e.into_inner());
1238                global_font_ctx
1239                    .collection
1240                    .register_fonts(font.clone(), None);
1241
1242                #[cfg(feature = "parallel-construct")]
1243                {
1244                    rayon::broadcast(|_ctx| {
1245                        let mut font_ctx = self
1246                            .thread_font_contexts
1247                            .get_or(|| RefCell::new(Box::new(global_font_ctx.clone())))
1248                            .borrow_mut();
1249                        font_ctx.collection.register_fonts(font.clone(), None);
1250                    });
1251                }
1252                drop(global_font_ctx);
1253
1254                // TODO: see if we can only invalidate if resolved fonts may have changed
1255                self.invalidate_inline_contexts();
1256            }
1257            Resource::None => {
1258                // Do nothing
1259            }
1260        }
1261    }
1262
1263    pub fn snapshot_node(&mut self, node_id: usize) {
1264        let Some(node) = self.nodes.get_mut(node_id) else {
1265            return;
1266        };
1267        let opaque_node_id = TNode::opaque(&&*node);
1268        node.has_snapshot = true;
1269        node.snapshot_handled
1270            .store(false, std::sync::atomic::Ordering::SeqCst);
1271
1272        // TODO: handle invalidations other than hover
1273        if let Some(_existing_snapshot) = self.snapshots.get_mut(&opaque_node_id) {
1274            // Do nothing
1275            // TODO: update snapshot
1276        } else {
1277            let attrs: Option<Vec<_>> = node.attrs().map(|attrs| {
1278                attrs
1279                    .iter()
1280                    .map(|attr| {
1281                        let ident = AttrIdentifier {
1282                            local_name: GenericAtomIdent(attr.name.local.clone()),
1283                            name: GenericAtomIdent(attr.name.local.clone()),
1284                            namespace: GenericAtomIdent(attr.name.ns.clone()),
1285                            prefix: None,
1286                        };
1287
1288                        let value = if attr.name.local == local_name!("id") {
1289                            AttrValue::Atom(Atom::from(&*attr.value))
1290                        } else if attr.name.local == local_name!("class") {
1291                            let classes = attr
1292                                .value
1293                                .split_ascii_whitespace()
1294                                .map(Atom::from)
1295                                .collect();
1296                            AttrValue::TokenList(attr.value.clone(), classes)
1297                        } else {
1298                            AttrValue::String(attr.value.clone())
1299                        };
1300
1301                        (ident, value)
1302                    })
1303                    .collect()
1304            });
1305
1306            let changed_attrs = attrs
1307                .as_ref()
1308                .map(|attrs| attrs.iter().map(|attr| attr.0.name.clone()).collect())
1309                .unwrap_or_default();
1310
1311            self.snapshots.insert(
1312                opaque_node_id,
1313                ServoElementSnapshot {
1314                    state: Some(node.element_state),
1315                    attrs,
1316                    changed_attrs,
1317                    class_changed: true,
1318                    id_changed: true,
1319                    other_attributes_changed: true,
1320                },
1321            );
1322        }
1323    }
1324
1325    pub fn snapshot_node_and(&mut self, node_id: usize, cb: impl FnOnce(&mut Node)) {
1326        self.snapshot_node(node_id);
1327        let Some(node) = self.nodes.get_mut(node_id) else {
1328            return;
1329        };
1330        cb(node);
1331    }
1332
1333    // Takes (x, y) co-ordinates (relative to the viewport)
1334    pub fn hit(&self, x: f32, y: f32) -> Option<HitResult> {
1335        // D-2c-followup: root_element widened to Option<&Node>; use `?` so a
1336        // missing root element returns `None` instead of unwrap-panicking.
1337        self.root_element()?.hit(x, y)
1338    }
1339
1340    pub fn focus_next_node(&mut self) -> Option<usize> {
1341        let focussed_node_id = self.get_focussed_node_id()?;
1342        let focussed_node = self.get_node(focussed_node_id)?;
1343        let id = self
1344            .next_node(focussed_node, |node| node.is_focussable())
1345            // Wrap around: when at the last focusable, continue from the document root
1346            .or_else(|| self.next_node(self.root_node(), |node| node.is_focussable()))?;
1347        self.set_focus_to(id);
1348        Some(id)
1349    }
1350
1351    /// Move focus to the previous focusable node in document order (reverse Tab).
1352    pub fn focus_prev_node(&mut self) -> Option<usize> {
1353        let focussed_node_id = self.get_focussed_node_id()?;
1354        let focussed_node = self.get_node(focussed_node_id)?;
1355        let id = self
1356            .prev_node(focussed_node, |node| node.is_focussable())
1357            // Wrap around: when at the first focusable, find the last focusable in the document
1358            .or_else(|| self.prev_node(self.root_node(), |node| node.is_focussable()))?;
1359        self.set_focus_to(id);
1360        Some(id)
1361    }
1362
1363    /// Clear the focussed node
1364    pub fn clear_focus(&mut self) {
1365        if let Some(id) = self.focus_node_id {
1366            let shell_provider = self.shell_provider.clone();
1367            self.snapshot_node_and(id, |node| node.blur(shell_provider));
1368            self.focus_node_id = None;
1369        }
1370    }
1371
1372    pub fn set_mousedown_node_id(&mut self, node_id: Option<usize>) {
1373        self.mousedown_node_id = node_id;
1374    }
1375    pub fn set_focus_to(&mut self, focus_node_id: usize) -> bool {
1376        if Some(focus_node_id) == self.focus_node_id {
1377            return false;
1378        }
1379
1380        #[cfg(feature = "tracing")]
1381        tracing::info!("Focussed node {focus_node_id}");
1382
1383        let shell_provider = self.shell_provider.clone();
1384
1385        // Remove focus from the old node
1386        if let Some(id) = self.focus_node_id {
1387            self.snapshot_node_and(id, |node| node.blur(shell_provider.clone()));
1388        }
1389
1390        // Focus the new node
1391        self.snapshot_node_and(focus_node_id, |node| node.focus(shell_provider));
1392
1393        self.focus_node_id = Some(focus_node_id);
1394
1395        true
1396    }
1397
1398    pub fn active_node(&mut self) -> bool {
1399        let Some(hover_node_id) = self.get_hover_node_id() else {
1400            return false;
1401        };
1402
1403        if let Some(active_node_id) = self.active_node_id {
1404            if active_node_id == hover_node_id {
1405                return true;
1406            }
1407            self.unactive_node();
1408        }
1409
1410        let active_node_id = Some(hover_node_id);
1411
1412        let node_path = self.maybe_node_layout_ancestors(active_node_id);
1413        for &id in node_path.iter() {
1414            self.snapshot_node_and(id, |node| node.active());
1415        }
1416
1417        self.active_node_id = active_node_id;
1418
1419        true
1420    }
1421
1422    pub fn unactive_node(&mut self) -> bool {
1423        let Some(active_node_id) = self.active_node_id.take() else {
1424            return false;
1425        };
1426
1427        let node_path = self.maybe_node_layout_ancestors(Some(active_node_id));
1428        for &id in node_path.iter() {
1429            self.snapshot_node_and(id, |node| node.unactive());
1430        }
1431
1432        true
1433    }
1434
1435    pub fn set_hover_to(&mut self, x: f32, y: f32) -> bool {
1436        let hit = self.hit(x, y);
1437        let hover_node_id = hit.map(|hit| hit.node_id);
1438        let new_is_text = hit.map(|hit| hit.is_text).unwrap_or(false);
1439
1440        // Return early if the new node is the same as the already-hovered node
1441        if hover_node_id == self.hover_node_id {
1442            return false;
1443        }
1444
1445        let old_node_path = self.maybe_node_layout_ancestors(self.hover_node_id);
1446        let new_node_path = self.maybe_node_layout_ancestors(hover_node_id);
1447        let same_count = old_node_path
1448            .iter()
1449            .zip(&new_node_path)
1450            .take_while(|(o, n)| o == n)
1451            .count();
1452        for &id in old_node_path.iter().skip(same_count) {
1453            self.snapshot_node_and(id, |node| node.unhover());
1454        }
1455        for &id in new_node_path.iter().skip(same_count) {
1456            self.snapshot_node_and(id, |node| node.hover());
1457        }
1458
1459        self.hover_node_id = hover_node_id;
1460        self.hover_node_is_text = new_is_text;
1461
1462        // Update the cursor
1463        let cursor = self.get_cursor().unwrap_or_default();
1464        self.shell_provider.set_cursor(cursor);
1465
1466        // Request redraw
1467        self.shell_provider.request_redraw();
1468
1469        true
1470    }
1471
1472    pub fn clear_hover(&mut self) -> bool {
1473        let Some(hover_node_id) = self.hover_node_id else {
1474            return false;
1475        };
1476
1477        let old_node_path = self.maybe_node_layout_ancestors(Some(hover_node_id));
1478        for &id in old_node_path.iter() {
1479            self.snapshot_node_and(id, |node| node.unhover());
1480        }
1481
1482        self.hover_node_id = None;
1483        self.hover_node_is_text = false;
1484
1485        // Update the cursor
1486        let cursor = self.get_cursor().unwrap_or_default();
1487        self.shell_provider.set_cursor(cursor);
1488
1489        // Request redraw
1490        self.shell_provider.request_redraw();
1491
1492        true
1493    }
1494
1495    pub fn get_hover_node_id(&self) -> Option<usize> {
1496        self.hover_node_id
1497    }
1498
1499    pub fn set_viewport(&mut self, viewport: Viewport) {
1500        let scale_has_changed = viewport.scale_f64() != self.viewport.scale_f64();
1501        self.viewport = viewport;
1502        self.set_stylist_device(make_device(&self.viewport, self.font_ctx.clone()));
1503        self.scroll_viewport_by(0.0, 0.0); // Clamp scroll offset
1504
1505        if scale_has_changed {
1506            self.invalidate_inline_contexts();
1507            self.shell_provider.request_redraw();
1508        }
1509    }
1510
1511    pub fn viewport(&self) -> &Viewport {
1512        &self.viewport
1513    }
1514
1515    pub fn viewport_mut(&mut self) -> ViewportMut<'_> {
1516        ViewportMut::new(self)
1517    }
1518
1519    pub fn zoom_by(&mut self, increment: f32) {
1520        *self.viewport.zoom_mut() += increment;
1521        self.set_viewport(self.viewport.clone());
1522    }
1523
1524    pub fn zoom_to(&mut self, zoom: f32) {
1525        *self.viewport.zoom_mut() = zoom;
1526        self.set_viewport(self.viewport.clone());
1527    }
1528
1529    pub fn get_viewport(&self) -> Viewport {
1530        self.viewport.clone()
1531    }
1532
1533    pub fn devtools(&self) -> &DevtoolSettings {
1534        &self.devtool_settings
1535    }
1536
1537    pub fn devtools_mut(&mut self) -> &mut DevtoolSettings {
1538        &mut self.devtool_settings
1539    }
1540
1541    pub fn is_animating(&self) -> bool {
1542        self.has_canvas
1543            | self.has_active_animations
1544            | self.subdoc_is_animating
1545            | (self.scroll_animation != ScrollAnimationState::None)
1546    }
1547
1548    /// Update the device and reset the stylist to process the new size
1549    pub fn set_stylist_device(&mut self, device: Device) {
1550        let origins = {
1551            let guard = &self.guard;
1552            let guards = StylesheetGuards {
1553                author: &guard.read(),
1554                ua_or_user: &guard.read(),
1555            };
1556            self.stylist.set_device(device, &guards)
1557        };
1558        self.stylist.force_stylesheet_origins_dirty(origins);
1559    }
1560
1561    pub fn stylist_device(&mut self) -> &Device {
1562        self.stylist.device()
1563    }
1564
1565    pub fn get_cursor(&self) -> Option<CursorIcon> {
1566        let node = self.get_node(self.get_hover_node_id()?)?;
1567
1568        if let Some(subdoc) = node.subdoc().map(|doc| doc.inner()) {
1569            return subdoc.get_cursor();
1570        }
1571
1572        let style = node.primary_styles()?;
1573        let keyword = stylo_to_cursor_icon(style.clone_cursor().keyword);
1574
1575        // Return cursor from style if it is non-auto
1576        if keyword != CursorIcon::Default {
1577            return Some(keyword);
1578        }
1579
1580        // Return text cursor for text inputs
1581        if node
1582            .element_data()
1583            .is_some_and(|e| e.text_input_data().is_some())
1584        {
1585            return Some(CursorIcon::Text);
1586        }
1587
1588        // Use "pointer" cursor if any ancestor is a link
1589        let mut maybe_node = Some(node);
1590        while let Some(node) = maybe_node {
1591            if node.is_link() {
1592                return Some(CursorIcon::Pointer);
1593            }
1594
1595            maybe_node = node.layout_parent.get().map(|node_id| node.with(node_id));
1596        }
1597
1598        // Return text cursor for text nodes
1599        if self.hover_node_is_text {
1600            return Some(CursorIcon::Text);
1601        }
1602
1603        // Else fallback to default cursor
1604        Some(CursorIcon::Default)
1605    }
1606
1607    pub fn scroll_node_by<F: FnMut(DomEvent)>(
1608        &mut self,
1609        node_id: usize,
1610        x: f64,
1611        y: f64,
1612        dispatch_event: F,
1613    ) {
1614        self.scroll_node_by_has_changed(node_id, x, y, dispatch_event);
1615    }
1616
1617    /// Scroll a node by given x and y
1618    /// Will bubble scrolling up to parent node once it can no longer scroll further
1619    /// If we're already at the root node, bubbles scrolling up to the viewport
1620    pub fn scroll_node_by_has_changed<F: FnMut(DomEvent)>(
1621        &mut self,
1622        node_id: usize,
1623        x: f64,
1624        y: f64,
1625        mut dispatch_event: F,
1626    ) -> bool {
1627        let Some(node) = self.nodes.get_mut(node_id) else {
1628            return false;
1629        };
1630
1631        let is_html_or_body = node.data.downcast_element().is_some_and(|e| {
1632            let tag = &e.name.local;
1633            tag == "html" || tag == "body"
1634        });
1635
1636        let (can_x_scroll, can_y_scroll) = node
1637            .primary_styles()
1638            .map(|styles| {
1639                (
1640                    matches!(styles.clone_overflow_x(), Overflow::Scroll | Overflow::Auto),
1641                    matches!(styles.clone_overflow_y(), Overflow::Scroll | Overflow::Auto)
1642                        || (styles.clone_overflow_y() == Overflow::Visible && is_html_or_body),
1643                )
1644            })
1645            .unwrap_or((false, false));
1646
1647        let initial = node.scroll_offset;
1648        let new_x = node.scroll_offset.x - x;
1649        let new_y = node.scroll_offset.y - y;
1650
1651        let mut bubble_x = 0.0;
1652        let mut bubble_y = 0.0;
1653
1654        let scroll_width = node.final_layout.scroll_width() as f64;
1655        let scroll_height = node.final_layout.scroll_height() as f64;
1656
1657        // Handle sub document case
1658        if let Some(mut sub_doc) = node.subdoc_mut().map(|doc| doc.inner_mut()) {
1659            let has_changed = if let Some(hover_node_id) = sub_doc.get_hover_node_id() {
1660                sub_doc.scroll_node_by_has_changed(hover_node_id, x, y, dispatch_event)
1661            } else {
1662                sub_doc.scroll_viewport_by_has_changed(x, y)
1663            };
1664
1665            // TODO: propagate remaining scroll to parent
1666            return has_changed;
1667        }
1668
1669        // If we're past our scroll bounds, transfer remainder of scrolling to parent/viewport
1670        if !can_x_scroll {
1671            bubble_x = x
1672        } else if new_x < 0.0 {
1673            bubble_x = -new_x;
1674            node.scroll_offset.x = 0.0;
1675        } else if new_x > scroll_width {
1676            bubble_x = scroll_width - new_x;
1677            node.scroll_offset.x = scroll_width;
1678        } else {
1679            node.scroll_offset.x = new_x;
1680        }
1681
1682        if !can_y_scroll {
1683            bubble_y = y
1684        } else if new_y < 0.0 {
1685            bubble_y = -new_y;
1686            node.scroll_offset.y = 0.0;
1687        } else if new_y > scroll_height {
1688            bubble_y = scroll_height - new_y;
1689            node.scroll_offset.y = scroll_height;
1690        } else {
1691            node.scroll_offset.y = new_y;
1692        }
1693
1694        let has_changed = node.scroll_offset != initial;
1695
1696        if has_changed {
1697            let layout = node.final_layout;
1698            let event = BlissScrollEvent {
1699                scroll_top: node.scroll_offset.y,
1700                scroll_left: node.scroll_offset.x,
1701                scroll_width: layout.scroll_width() as i32,
1702                scroll_height: layout.scroll_height() as i32,
1703                client_width: layout.size.width as i32,
1704                client_height: layout.size.height as i32,
1705            };
1706
1707            dispatch_event(DomEvent::new(node_id, DomEventData::Scroll(event)));
1708        }
1709
1710        if bubble_x != 0.0 || bubble_y != 0.0 {
1711            if let Some(parent) = node.parent {
1712                return self.scroll_node_by_has_changed(parent, bubble_x, bubble_y, dispatch_event)
1713                    | has_changed;
1714            } else {
1715                return self.scroll_viewport_by_has_changed(bubble_x, bubble_y) | has_changed;
1716            }
1717        }
1718
1719        has_changed
1720    }
1721
1722    pub fn scroll_viewport_by(&mut self, x: f64, y: f64) {
1723        self.scroll_viewport_by_has_changed(x, y);
1724    }
1725
1726    /// Scroll the viewport by the given values
1727    pub fn scroll_viewport_by_has_changed(&mut self, x: f64, y: f64) -> bool {
1728        // D-2c-followup: root_element widened to Option<&Node>; degrade to a
1729        // default (zero) content size when no root element is mounted.
1730        let content_size = self.root_element().map(|r| r.final_layout.size).unwrap_or_default();
1731        let new_scroll = (self.viewport_scroll.x - x, self.viewport_scroll.y - y);
1732        let window_width = self.viewport.window_size.0 as f64 / self.viewport.scale() as f64;
1733        let window_height = self.viewport.window_size.1 as f64 / self.viewport.scale() as f64;
1734
1735        let initial = self.viewport_scroll;
1736        self.viewport_scroll.x = f64::max(
1737            0.0,
1738            f64::min(new_scroll.0, content_size.width as f64 - window_width),
1739        );
1740        self.viewport_scroll.y = f64::max(
1741            0.0,
1742            f64::min(new_scroll.1, content_size.height as f64 - window_height),
1743        );
1744
1745        self.viewport_scroll != initial
1746    }
1747
1748    pub fn scroll_by(
1749        &mut self,
1750        anchor_node_id: Option<usize>,
1751        scroll_x: f64,
1752        scroll_y: f64,
1753        dispatch_event: &mut dyn FnMut(DomEvent),
1754    ) -> bool {
1755        if let Some(anchor_node_id) = anchor_node_id {
1756            self.scroll_node_by_has_changed(anchor_node_id, scroll_x, scroll_y, dispatch_event)
1757        } else {
1758            self.scroll_viewport_by_has_changed(scroll_x, scroll_y)
1759        }
1760    }
1761
1762    pub fn viewport_scroll(&self) -> crate::Point<f64> {
1763        self.viewport_scroll
1764    }
1765
1766    pub fn set_viewport_scroll(&mut self, scroll: crate::Point<f64>) {
1767        self.viewport_scroll = scroll;
1768    }
1769
1770    /// Computes the size and position of the `Node` relative to the viewport
1771    pub fn get_client_bounding_rect(&self, node_id: usize) -> Option<BoundingRect> {
1772        let node = self.get_node(node_id)?;
1773        let pos = node.absolute_position(0.0, 0.0);
1774
1775        Some(BoundingRect {
1776            x: pos.x as f64 - self.viewport_scroll.x,
1777            y: pos.y as f64 - self.viewport_scroll.y,
1778            width: node.unrounded_layout.size.width as f64,
1779            height: node.unrounded_layout.size.height as f64,
1780        })
1781    }
1782
1783    pub fn find_title_node(&self) -> Option<&Node> {
1784        TreeTraverser::new(self)
1785            .find(|node_id| {
1786                self.nodes[*node_id]
1787                    .data
1788                    .is_element_with_tag_name(&local_name!("title"))
1789            })
1790            .map(|node_id| &self.nodes[node_id])
1791    }
1792
1793    pub fn with_text_input(
1794        &mut self,
1795        node_id: usize,
1796        cb: impl FnOnce(PlainEditorDriver<TextBrush>),
1797    ) {
1798        let Some(node) = self.nodes.get_mut(node_id) else {
1799            return;
1800        };
1801
1802        if let Some(text_input) = node
1803            .element_data_mut()
1804            .and_then(|el| el.text_input_data_mut())
1805        {
1806            let mut font_ctx = self.font_ctx.lock().unwrap_or_else(|e| e.into_inner());
1807            let layout_ctx = &mut self.layout_ctx;
1808            let driver = text_input.editor.driver(&mut font_ctx, layout_ctx);
1809            cb(driver)
1810        }
1811    }
1812
1813    pub(crate) fn compute_has_canvas(&self) -> bool {
1814        TreeTraverser::new(self).any(|node_id| {
1815            let node = &self.nodes[node_id];
1816            let Some(element) = node.element_data() else {
1817                return false;
1818            };
1819            if element.name.local == local_name!("canvas") && element.has_attr(local_name!("src")) {
1820                return true;
1821            }
1822
1823            false
1824        })
1825    }
1826
1827    // Text selection methods
1828
1829    /// Find the text position (inline_root_id, byte_offset) at a given point.
1830    /// Uses hit() for proper coordinate transformation, then finds the inline root
1831    /// and byte offset.
1832    pub fn find_text_position(&self, x: f32, y: f32) -> Option<(usize, usize)> {
1833        let hit = self.hit(x, y)?;
1834        let hit_node = self.get_node(hit.node_id)?;
1835        let inline_root = hit_node.inline_root_ancestor()?;
1836        let byte_offset = inline_root.text_offset_at_point(hit.x, hit.y)?;
1837        Some((inline_root.id, byte_offset))
1838    }
1839
1840    /// Set the text selection range (creates a new selection from anchor to focus)
1841    pub fn set_text_selection(
1842        &mut self,
1843        anchor_node: usize,
1844        anchor_offset: usize,
1845        focus_node: usize,
1846        focus_offset: usize,
1847    ) {
1848        self.text_selection =
1849            TextSelection::new(anchor_node, anchor_offset, focus_node, focus_offset);
1850
1851        // For anonymous blocks, switch to storing parent+sibling_index (stable reference)
1852        if let (Some(parent), Some(idx)) = self.anonymous_block_location(anchor_node) {
1853            self.text_selection
1854                .anchor
1855                .set_anonymous(parent, idx, anchor_offset);
1856        }
1857        if let (Some(parent), Some(idx)) = self.anonymous_block_location(focus_node) {
1858            self.text_selection
1859                .focus
1860                .set_anonymous(parent, idx, focus_offset);
1861        }
1862    }
1863
1864    /// Get the parent ID and sibling index for a node if it's an anonymous block.
1865    /// Returns (None, None) for non-anonymous blocks.
1866    fn anonymous_block_location(&self, node_id: usize) -> (Option<usize>, Option<usize>) {
1867        let Some(node) = self.get_node(node_id) else {
1868            return (None, None);
1869        };
1870
1871        if !node.is_anonymous() {
1872            return (None, None);
1873        }
1874
1875        let Some(parent_id) = node.parent else {
1876            return (None, None);
1877        };
1878
1879        let Some(parent) = self.get_node(parent_id) else {
1880            return (Some(parent_id), None);
1881        };
1882
1883        let layout_children = parent.layout_children.borrow();
1884        let Some(children) = layout_children.as_ref() else {
1885            return (Some(parent_id), None);
1886        };
1887
1888        // Find the index of this anonymous block among siblings
1889        let mut anon_index = 0;
1890        for &child_id in children.iter() {
1891            if child_id == node_id {
1892                return (Some(parent_id), Some(anon_index));
1893            }
1894            if self.get_node(child_id).is_some_and(|n| n.is_anonymous()) {
1895                anon_index += 1;
1896            }
1897        }
1898
1899        (Some(parent_id), None)
1900    }
1901
1902    /// Clear the text selection
1903    pub fn clear_text_selection(&mut self) {
1904        self.text_selection.clear();
1905    }
1906
1907    /// Update the selection focus point (used during mouse drag to extend selection).
1908    pub fn update_selection_focus(&mut self, focus_node: usize, focus_offset: usize) {
1909        // For anonymous blocks, store parent+sibling_index; otherwise store node directly
1910        if let (Some(parent), Some(idx)) = self.anonymous_block_location(focus_node) {
1911            self.text_selection
1912                .focus
1913                .set_anonymous(parent, idx, focus_offset);
1914        } else {
1915            self.text_selection.set_focus(focus_node, focus_offset);
1916        }
1917    }
1918
1919    /// Extend text selection to the given point. Returns true if selection was updated.
1920    /// This is a convenience method that combines find_text_position and update_selection_focus.
1921    pub fn extend_text_selection_to_point(&mut self, x: f32, y: f32) -> bool {
1922        if !self.text_selection.anchor.is_some() {
1923            return false;
1924        }
1925
1926        if let Some((node, offset)) = self.find_text_position(x, y) {
1927            self.update_selection_focus(node, offset);
1928            self.shell_provider.request_redraw();
1929            true
1930        } else {
1931            false
1932        }
1933    }
1934
1935    /// Find the Nth anonymous block under a parent.
1936    fn find_anonymous_block_by_index(
1937        &self,
1938        parent_id: usize,
1939        target_index: usize,
1940    ) -> Option<usize> {
1941        let parent = self.get_node(parent_id)?;
1942        let layout_children = parent.layout_children.borrow();
1943        let children = layout_children.as_ref()?;
1944
1945        children
1946            .iter()
1947            .filter(|&&child_id| self.get_node(child_id).is_some_and(|n| n.is_anonymous()))
1948            .nth(target_index)
1949            .copied()
1950    }
1951
1952    /// Check if there is an active (non-empty) text selection
1953    pub fn has_text_selection(&self) -> bool {
1954        self.text_selection.is_active()
1955    }
1956
1957    /// Get the selected text content, supporting selection across multiple inline roots.
1958    pub fn get_selected_text(&self) -> Option<String> {
1959        let ranges = self.get_text_selection_ranges();
1960        if ranges.is_empty() {
1961            return None;
1962        }
1963
1964        let mut result = String::new();
1965        for (node_id, start, end) in &ranges {
1966            let node = self.get_node(*node_id)?;
1967            let element_data = node.element_data()?;
1968            let inline_layout = element_data.inline_layout_data.as_ref()?;
1969
1970            if *end > inline_layout.text.len() {
1971                continue;
1972            }
1973
1974            if !result.is_empty() {
1975                result.push(' ');
1976            }
1977            result.push_str(&inline_layout.text[*start..*end]);
1978        }
1979
1980        if result.is_empty() {
1981            None
1982        } else {
1983            Some(result)
1984        }
1985    }
1986
1987    /// Get all selection ranges as Vec<(node_id, start_offset, end_offset)>.
1988    /// Returns empty vec if no selection.
1989    pub fn get_text_selection_ranges(&self) -> Vec<(usize, usize, usize)> {
1990        let lookup = |parent_id, idx| self.find_anonymous_block_by_index(parent_id, idx);
1991
1992        let anchor_node = match self.text_selection.anchor.resolve_node_id(lookup) {
1993            Some(id) => id,
1994            None => return Vec::new(),
1995        };
1996        let focus_node = match self.text_selection.focus.resolve_node_id(lookup) {
1997            Some(id) => id,
1998            None => return Vec::new(),
1999        };
2000
2001        // Single node selection
2002        if anchor_node == focus_node {
2003            let start = self
2004                .text_selection
2005                .anchor
2006                .offset
2007                .min(self.text_selection.focus.offset);
2008            let end = self
2009                .text_selection
2010                .anchor
2011                .offset
2012                .max(self.text_selection.focus.offset);
2013
2014            if start == end {
2015                return Vec::new();
2016            }
2017            return vec![(anchor_node, start, end)];
2018        }
2019
2020        // Multi-node selection: collect all inline roots between anchor and focus
2021        let inline_roots = self.collect_inline_roots_in_range(anchor_node, focus_node);
2022        if inline_roots.is_empty() {
2023            return Vec::new();
2024        }
2025
2026        // Determine document order using the collected inline_roots order
2027        // (inline_roots is already in document order from first to last)
2028        let first_in_roots = inline_roots[0];
2029
2030        let (first_node, first_offset, last_node, last_offset) =
2031            if first_in_roots == anchor_node || (first_in_roots != focus_node) {
2032                // anchor is first (or neither endpoint is in roots, which shouldn't happen)
2033                (
2034                    anchor_node,
2035                    self.text_selection.anchor.offset,
2036                    focus_node,
2037                    self.text_selection.focus.offset,
2038                )
2039            } else {
2040                // focus is first
2041                (
2042                    focus_node,
2043                    self.text_selection.focus.offset,
2044                    anchor_node,
2045                    self.text_selection.anchor.offset,
2046                )
2047            };
2048
2049        let mut ranges = Vec::with_capacity(inline_roots.len());
2050
2051        for &node_id in &inline_roots {
2052            let Some(node) = self.get_node(node_id) else {
2053                continue;
2054            };
2055            let Some(element_data) = node.element_data() else {
2056                continue;
2057            };
2058            let Some(inline_layout) = element_data.inline_layout_data.as_ref() else {
2059                continue;
2060            };
2061
2062            let text_len = inline_layout.text.len();
2063
2064            if node_id == first_node && node_id == last_node {
2065                let start = first_offset.min(last_offset);
2066                let end = first_offset.max(last_offset);
2067                if start < end && end <= text_len {
2068                    ranges.push((node_id, start, end));
2069                }
2070            } else if node_id == first_node {
2071                if first_offset < text_len {
2072                    ranges.push((node_id, first_offset, text_len));
2073                }
2074            } else if node_id == last_node {
2075                if last_offset > 0 && last_offset <= text_len {
2076                    ranges.push((node_id, 0, last_offset));
2077                }
2078            } else if text_len > 0 {
2079                ranges.push((node_id, 0, text_len));
2080            }
2081        }
2082
2083        ranges
2084    }
2085}
2086
2087pub struct BoundingRect {
2088    pub x: f64,
2089    pub y: f64,
2090    pub width: f64,
2091    pub height: f64,
2092}
2093
2094impl AsRef<BaseDocument> for BaseDocument {
2095    fn as_ref(&self) -> &BaseDocument {
2096        self
2097    }
2098}
2099
2100impl AsMut<BaseDocument> for BaseDocument {
2101    fn as_mut(&mut self) -> &mut BaseDocument {
2102        self
2103    }
2104}