blitz_dom/
mutator.rs

1use std::collections::HashSet;
2use std::mem;
3use std::ops::{Deref, DerefMut};
4
5use crate::document::make_device;
6use crate::net::{CssHandler, ImageHandler};
7use crate::node::{CanvasData, NodeFlags, SpecialElementData};
8use crate::util::ImageType;
9use crate::{
10    Attribute, BaseDocument, ElementData, Node, NodeData, QualName, local_name, qual_name,
11};
12use blitz_traits::net::Request;
13use blitz_traits::shell::Viewport;
14use style::Atom;
15use style::invalidation::element::restyle_hints::RestyleHint;
16use style::stylesheets::OriginSet;
17
18macro_rules! tag_and_attr {
19    ($tag:tt, $attr:tt) => {
20        (&local_name!($tag), &local_name!($attr))
21    };
22}
23
24#[derive(Debug, Clone)]
25pub enum AppendTextErr {
26    /// The node is not a text node
27    NotTextNode,
28}
29
30/// Operations that happen almost immediately, but are deferred within a
31/// function for borrow-checker reasons.
32enum SpecialOp {
33    LoadImage(usize),
34    LoadStylesheet(usize),
35    UnloadStylesheet(usize),
36    LoadCustomPaintSource(usize),
37    ProcessButtonInput(usize),
38}
39
40pub struct DocumentMutator<'doc> {
41    /// Document is public as an escape hatch, but users of this API should ideally avoid using it
42    /// and prefer exposing additional functionality in DocumentMutator.
43    pub doc: &'doc mut BaseDocument,
44
45    eager_op_queue: Vec<SpecialOp>,
46
47    // Tracked nodes for deferred processing when mutations have completed
48    title_node: Option<usize>,
49    style_nodes: HashSet<usize>,
50    form_nodes: HashSet<usize>,
51
52    /// Whether an element/attribute that affect animation status has been seen
53    recompute_is_animating: bool,
54
55    /// The (latest) node which has been mounted in and had autofocus=true, if any
56    #[cfg(feature = "autofocus")]
57    node_to_autofocus: Option<usize>,
58}
59
60impl Drop for DocumentMutator<'_> {
61    fn drop(&mut self) {
62        self.flush(); // Defined at bottom of file
63    }
64}
65
66impl DocumentMutator<'_> {
67    pub fn new<'doc>(doc: &'doc mut BaseDocument) -> DocumentMutator<'doc> {
68        DocumentMutator {
69            doc,
70            eager_op_queue: Vec::new(),
71            title_node: None,
72            style_nodes: HashSet::new(),
73            form_nodes: HashSet::new(),
74            recompute_is_animating: false,
75            #[cfg(feature = "autofocus")]
76            node_to_autofocus: None,
77        }
78    }
79
80    // Query methods
81
82    pub fn node_has_parent(&self, node_id: usize) -> bool {
83        self.doc.nodes[node_id].parent.is_some()
84    }
85
86    pub fn previous_sibling_id(&self, node_id: usize) -> Option<usize> {
87        self.doc.nodes[node_id].backward(1).map(|node| node.id)
88    }
89
90    pub fn next_sibling_id(&self, node_id: usize) -> Option<usize> {
91        self.doc.nodes[node_id].forward(1).map(|node| node.id)
92    }
93
94    pub fn parent_id(&self, node_id: usize) -> Option<usize> {
95        self.doc.nodes[node_id].parent
96    }
97
98    pub fn last_child_id(&self, node_id: usize) -> Option<usize> {
99        self.doc.nodes[node_id].children.last().copied()
100    }
101
102    pub fn child_ids(&self, node_id: usize) -> Vec<usize> {
103        self.doc.nodes[node_id].children.clone()
104    }
105
106    pub fn element_name(&self, node_id: usize) -> Option<&QualName> {
107        self.doc.nodes[node_id].element_data().map(|el| &el.name)
108    }
109
110    pub fn node_at_path(&self, start_node_id: usize, path: &[u8]) -> usize {
111        let mut current = &self.doc.nodes[start_node_id];
112        for i in path {
113            let new_id = current.children[*i as usize];
114            current = &self.doc.nodes[new_id];
115        }
116        current.id
117    }
118
119    // Node creation methods
120
121    pub fn create_comment_node(&mut self) -> usize {
122        self.doc.create_node(NodeData::Comment)
123    }
124
125    pub fn create_text_node(&mut self, text: &str) -> usize {
126        self.doc.create_text_node(text)
127    }
128
129    pub fn create_element(&mut self, name: QualName, attrs: Vec<Attribute>) -> usize {
130        let mut data = ElementData::new(name, attrs);
131        data.flush_style_attribute(self.doc.guard(), &self.doc.url.url_extra_data());
132
133        let id = self.doc.create_node(NodeData::Element(data));
134        let node = self.doc.get_node(id).unwrap();
135
136        // Initialise style data
137        *node.stylo_element_data.borrow_mut() = Some(Default::default());
138
139        id
140    }
141
142    pub fn deep_clone_node(&mut self, node_id: usize) -> usize {
143        self.doc.deep_clone_node(node_id)
144    }
145
146    // Node mutation methods
147
148    pub fn set_node_text(&mut self, node_id: usize, value: &str) {
149        let node = self.doc.get_node_mut(node_id).unwrap();
150
151        let text = match node.data {
152            NodeData::Text(ref mut text) => text,
153            // TODO: otherwise this is basically element.textContent which is a bit different - need to parse as html
154            _ => return,
155        };
156
157        let changed = text.content != value;
158        if changed {
159            text.content.clear();
160            text.content.push_str(value);
161            let parent = node.parent;
162            self.maybe_record_node(parent);
163        }
164    }
165
166    pub fn append_text_to_node(&mut self, node_id: usize, text: &str) -> Result<(), AppendTextErr> {
167        match self.doc.nodes[node_id].text_data_mut() {
168            Some(data) => {
169                data.content += text;
170                Ok(())
171            }
172            None => Err(AppendTextErr::NotTextNode),
173        }
174    }
175
176    pub fn add_attrs_if_missing(&mut self, node_id: usize, attrs: Vec<Attribute>) {
177        let node = &mut self.doc.nodes[node_id];
178        let element_data = node.element_data_mut().expect("Not an element");
179
180        let existing_names = element_data
181            .attrs
182            .iter()
183            .map(|e| e.name.clone())
184            .collect::<HashSet<_>>();
185
186        for attr in attrs
187            .into_iter()
188            .filter(|attr| !existing_names.contains(&attr.name))
189        {
190            self.set_attribute(node_id, attr.name, &attr.value);
191        }
192    }
193
194    pub fn set_attribute(&mut self, node_id: usize, name: QualName, value: &str) {
195        self.doc.snapshot_node(node_id);
196
197        let node = &mut self.doc.nodes[node_id];
198        if let Some(data) = &mut *node.stylo_element_data.borrow_mut() {
199            data.hint |= RestyleHint::restyle_subtree();
200        }
201
202        let NodeData::Element(ref mut element) = node.data else {
203            return;
204        };
205
206        element.attrs.set(name.clone(), value);
207
208        let tag = &element.name.local;
209        let attr = &name.local;
210
211        if *attr == local_name!("id") {
212            element.id = Some(Atom::from(value))
213        }
214
215        if *attr == local_name!("value") {
216            if let Some(input_data) = element.text_input_data_mut() {
217                // Update text input value
218                input_data.set_text(
219                    &mut self.doc.font_ctx.lock().unwrap(),
220                    &mut self.doc.layout_ctx,
221                    value,
222                );
223            }
224            return;
225        }
226
227        if *attr == local_name!("style") {
228            element.flush_style_attribute(&self.doc.guard, &self.doc.url.url_extra_data());
229            return;
230        }
231
232        // If node if not in the document, then don't apply any special behaviours
233        // and simply set the attribute value
234        if !node.flags.is_in_document() {
235            return;
236        }
237
238        if (tag, attr) == tag_and_attr!("input", "checked") {
239            set_input_checked_state(element, value.to_string());
240        } else if (tag, attr) == tag_and_attr!("img", "src") {
241            self.load_image(node_id);
242        } else if (tag, attr) == tag_and_attr!("canvas", "src") {
243            self.load_custom_paint_src(node_id);
244        }
245    }
246
247    pub fn clear_attribute(&mut self, node_id: usize, name: QualName) {
248        self.doc.snapshot_node(node_id);
249
250        let node = &mut self.doc.nodes[node_id];
251
252        let mut stylo_element_data = node.stylo_element_data.borrow_mut();
253        if let Some(data) = &mut *stylo_element_data {
254            data.hint |= RestyleHint::restyle_subtree();
255        }
256        drop(stylo_element_data);
257
258        let Some(element) = node.element_data_mut() else {
259            return;
260        };
261
262        let removed_attr = element.attrs.remove(&name);
263        let had_attr = removed_attr.is_some();
264        if !had_attr {
265            return;
266        }
267
268        if name.local == local_name!("id") {
269            element.id = None;
270        }
271
272        // Update text input value
273        if name.local == local_name!("value") {
274            if let Some(input_data) = element.text_input_data_mut() {
275                input_data.set_text(
276                    &mut self.doc.font_ctx.lock().unwrap(),
277                    &mut self.doc.layout_ctx,
278                    "",
279                );
280            }
281        }
282
283        let tag = &element.name.local;
284        let attr = &name.local;
285        if *attr == local_name!("style") {
286            element.flush_style_attribute(&self.doc.guard, &self.doc.url.url_extra_data());
287        } else if (tag, attr) == tag_and_attr!("canvas", "src") {
288            self.recompute_is_animating = true;
289        } else if (tag, attr) == tag_and_attr!("link", "href") {
290            self.unload_stylesheet(node_id);
291        }
292    }
293
294    pub fn set_style_property(&mut self, node_id: usize, name: &str, value: &str) {
295        self.doc.set_style_property(node_id, name, value)
296    }
297
298    pub fn remove_style_property(&mut self, node_id: usize, name: &str) {
299        self.doc.remove_style_property(node_id, name)
300    }
301
302    /// Remove the node from it's parent but don't drop it
303    pub fn remove_node(&mut self, node_id: usize) {
304        let node = &mut self.doc.nodes[node_id];
305
306        // Update child_idx values
307        if let Some(parent_id) = node.parent.take() {
308            let parent = &mut self.doc.nodes[parent_id];
309            parent.children.retain(|id| *id != node_id);
310            self.maybe_record_node(parent_id);
311        }
312
313        self.process_removed_subtree(node_id);
314    }
315
316    fn remove_node_ignoring_parent(&mut self, node_id: usize) -> Option<Node> {
317        let mut node = self.doc.nodes.try_remove(node_id);
318        if let Some(node) = &mut node {
319            for &child in &node.children {
320                self.remove_node_ignoring_parent(child);
321            }
322        }
323        node
324    }
325
326    pub fn remove_and_drop_node(&mut self, node_id: usize) -> Option<Node> {
327        self.process_removed_subtree(node_id);
328
329        let node = self.remove_node_ignoring_parent(node_id);
330
331        // Update child_idx values
332        if let Some(parent_id) = node.as_ref().and_then(|node| node.parent) {
333            let parent = &mut self.doc.nodes[parent_id];
334            parent.children.retain(|id| *id != node_id);
335            self.maybe_record_node(parent_id);
336        }
337
338        node
339    }
340
341    pub fn remove_and_drop_all_children(&mut self, node_id: usize) {
342        let children = mem::take(&mut self.doc.nodes[node_id].children);
343        for child_id in children {
344            self.process_removed_subtree(child_id);
345            let _ = self.remove_node_ignoring_parent(child_id);
346        }
347        self.maybe_record_node(node_id);
348    }
349
350    // Tree mutation methods
351    pub fn remove_node_if_unparented(&mut self, node_id: usize) {
352        if let Some(node) = self.doc.get_node(node_id) {
353            if node.parent.is_none() {
354                self.remove_and_drop_node(node_id);
355            }
356        }
357    }
358
359    /// Remove all of the children from old_parent_id and append them to new_parent_id
360    pub fn append_children(&mut self, parent_id: usize, child_ids: &[usize]) {
361        self.add_children_to_parent(parent_id, child_ids, &|parent, child_ids| {
362            parent.children.extend_from_slice(child_ids);
363        });
364    }
365
366    pub fn insert_nodes_before(&mut self, anchor_node_id: usize, new_node_ids: &[usize]) {
367        let parent_id = self.doc.nodes[anchor_node_id].parent.unwrap();
368        self.add_children_to_parent(parent_id, new_node_ids, &|parent, child_ids| {
369            let node_child_idx = parent.index_of_child(anchor_node_id).unwrap();
370            parent
371                .children
372                .splice(node_child_idx..node_child_idx, child_ids.iter().copied());
373        });
374    }
375
376    fn add_children_to_parent(
377        &mut self,
378        parent_id: usize,
379        child_ids: &[usize],
380        insert_children_fn: &dyn Fn(&mut Node, &[usize]),
381    ) {
382        let new_parent = &mut self.doc.nodes[parent_id];
383        let new_parent_is_in_doc = new_parent.flags.is_in_document();
384
385        insert_children_fn(new_parent, child_ids);
386
387        for child_id in child_ids.iter().copied() {
388            let child = &mut self.doc.nodes[child_id];
389            let old_parent_id = child.parent.replace(parent_id);
390
391            let child_was_in_doc = child.flags.is_in_document();
392            if new_parent_is_in_doc != child_was_in_doc {
393                self.process_added_subtree(child_id);
394            }
395
396            if let Some(old_parent_id) = old_parent_id {
397                let old_parent = &mut self.doc.nodes[old_parent_id];
398
399                old_parent.children.retain(|id| *id != child_id);
400                self.maybe_record_node(old_parent_id);
401            }
402        }
403
404        self.maybe_record_node(parent_id);
405    }
406
407    // Tree mutation methods (that defer to other methods)
408    pub fn insert_nodes_after(&mut self, anchor_node_id: usize, new_node_ids: &[usize]) {
409        match self.next_sibling_id(anchor_node_id) {
410            Some(id) => self.insert_nodes_before(id, new_node_ids),
411            None => {
412                let parent_id = self.parent_id(anchor_node_id).unwrap();
413                self.append_children(parent_id, new_node_ids)
414            }
415        }
416    }
417
418    pub fn reparent_children(&mut self, old_parent_id: usize, new_parent_id: usize) {
419        let child_ids = std::mem::take(&mut self.doc.nodes[old_parent_id].children);
420        self.maybe_record_node(old_parent_id);
421        self.append_children(new_parent_id, &child_ids);
422    }
423
424    pub fn replace_node_with(&mut self, anchor_node_id: usize, new_node_ids: &[usize]) {
425        self.insert_nodes_before(anchor_node_id, new_node_ids);
426        self.remove_node(anchor_node_id);
427    }
428}
429
430impl<'doc> DocumentMutator<'doc> {
431    pub fn flush(&mut self) {
432        if self.recompute_is_animating {
433            self.doc.is_animating = self.doc.compute_is_animating();
434        }
435
436        if let Some(id) = self.title_node {
437            let title = self.doc.nodes[id].text_content();
438            self.doc.shell_provider.set_window_title(title);
439        }
440
441        // Add/Update inline stylesheets (<style> elements)
442        for id in self.style_nodes.drain() {
443            self.doc.process_style_element(id);
444        }
445
446        for id in self.form_nodes.drain() {
447            self.doc.reset_form_owner(id);
448        }
449
450        #[cfg(feature = "autofocus")]
451        if let Some(node_id) = self.node_to_autofocus.take() {
452            if self.doc.get_node(node_id).is_some() {
453                self.doc.set_focus_to(node_id);
454            }
455        }
456    }
457
458    pub fn set_inner_html(&mut self, node_id: usize, html: &str) {
459        self.remove_and_drop_all_children(node_id);
460        self.doc
461            .html_parser_provider
462            .clone()
463            .parse_inner_html(self, node_id, html);
464    }
465
466    fn flush_eager_ops(&mut self) {
467        let mut ops = mem::take(&mut self.eager_op_queue);
468        for op in ops.drain(0..) {
469            match op {
470                SpecialOp::LoadImage(node_id) => self.load_image(node_id),
471                SpecialOp::LoadStylesheet(node_id) => self.load_linked_stylesheet(node_id),
472                SpecialOp::UnloadStylesheet(node_id) => self.unload_stylesheet(node_id),
473                SpecialOp::LoadCustomPaintSource(node_id) => self.load_custom_paint_src(node_id),
474                SpecialOp::ProcessButtonInput(node_id) => self.process_button_input(node_id),
475            }
476        }
477
478        // Queue is empty, but put Vec back anyway so allocation can be reused.
479        self.eager_op_queue = ops;
480    }
481
482    fn process_added_subtree(&mut self, node_id: usize) {
483        self.doc.iter_subtree_mut(node_id, |node_id, doc| {
484            let node = &mut doc.nodes[node_id];
485            node.flags.set(NodeFlags::IS_IN_DOCUMENT, true);
486
487            // If the node has an "id" attribute, store it in the ID map.
488            if let Some(id_attr) = node.attr(local_name!("id")) {
489                doc.nodes_to_id.insert(id_attr.to_string(), node_id);
490            }
491
492            let NodeData::Element(ref mut element) = node.data else {
493                return;
494            };
495
496            // Custom post-processing by element tag name
497            let tag = element.name.local.as_ref();
498            match tag {
499                "title" => self.title_node = Some(node_id),
500                "link" => self.eager_op_queue.push(SpecialOp::LoadStylesheet(node_id)),
501                "img" => self.eager_op_queue.push(SpecialOp::LoadImage(node_id)),
502                "canvas" => self
503                    .eager_op_queue
504                    .push(SpecialOp::LoadCustomPaintSource(node_id)),
505                "style" => {
506                    self.style_nodes.insert(node_id);
507                }
508                "button" | "fieldset" | "input" | "select" | "textarea" | "object" | "output" => {
509                    self.eager_op_queue
510                        .push(SpecialOp::ProcessButtonInput(node_id));
511                    self.form_nodes.insert(node_id);
512                }
513                _ => {}
514            }
515
516            #[cfg(feature = "autofocus")]
517            if node.is_focussable() {
518                if let NodeData::Element(ref element) = node.data {
519                    if let Some(value) = element.attr(local_name!("autofocus")) {
520                        if value == "true" {
521                            self.node_to_autofocus = Some(node_id);
522                        }
523                    }
524                }
525            }
526        });
527
528        self.flush_eager_ops();
529    }
530
531    fn process_removed_subtree(&mut self, node_id: usize) {
532        self.doc.iter_subtree_mut(node_id, |node_id, doc| {
533            let node = &mut doc.nodes[node_id];
534            node.flags.set(NodeFlags::IS_IN_DOCUMENT, false);
535
536            // If the node has an "id" attribute remove it from the ID map.
537            if let Some(id_attr) = node.attr(local_name!("id")) {
538                doc.nodes_to_id.remove(id_attr);
539            }
540
541            let NodeData::Element(ref mut element) = node.data else {
542                return;
543            };
544
545            match &element.special_data {
546                SpecialElementData::Stylesheet(_) => self
547                    .eager_op_queue
548                    .push(SpecialOp::UnloadStylesheet(node_id)),
549                SpecialElementData::Image(_) => {}
550                SpecialElementData::Canvas(_) => {
551                    self.recompute_is_animating = true;
552                }
553                SpecialElementData::TableRoot(_) => {}
554                SpecialElementData::TextInput(_) => {}
555                SpecialElementData::CheckboxInput(_) => {}
556                #[cfg(feature = "file_input")]
557                SpecialElementData::FileInput(_) => {}
558                SpecialElementData::None => {}
559            }
560        });
561
562        self.flush_eager_ops();
563    }
564
565    fn maybe_record_node(&mut self, node_id: impl Into<Option<usize>>) {
566        let Some(node_id) = node_id.into() else {
567            return;
568        };
569
570        let Some(tag_name) = self.doc.nodes[node_id]
571            .data
572            .downcast_element()
573            .map(|elem| &elem.name.local)
574        else {
575            return;
576        };
577
578        match tag_name.as_ref() {
579            "title" => self.title_node = Some(node_id),
580            "style" => {
581                self.style_nodes.insert(node_id);
582            }
583            _ => {}
584        }
585    }
586
587    fn load_linked_stylesheet(&mut self, target_id: usize) {
588        let node = &self.doc.nodes[target_id];
589
590        let rel_attr = node.attr(local_name!("rel"));
591        let href_attr = node.attr(local_name!("href"));
592
593        let (Some(rels), Some(href)) = (rel_attr, href_attr) else {
594            return;
595        };
596        if !rels.split_ascii_whitespace().any(|rel| rel == "stylesheet") {
597            return;
598        }
599
600        let url = self.doc.resolve_url(href);
601        self.doc.net_provider.fetch(
602            self.doc.id(),
603            Request::get(url.clone()),
604            Box::new(CssHandler {
605                node: target_id,
606                source_url: url,
607                guard: self.doc.guard.clone(),
608                provider: self.doc.net_provider.clone(),
609            }),
610        );
611    }
612
613    fn unload_stylesheet(&mut self, node_id: usize) {
614        let node = &mut self.doc.nodes[node_id];
615        let Some(element) = node.element_data_mut() else {
616            unreachable!();
617        };
618        let SpecialElementData::Stylesheet(stylesheet) = element.special_data.take() else {
619            unreachable!();
620        };
621
622        let guard = self.doc.guard.read();
623        self.doc.stylist.remove_stylesheet(stylesheet, &guard);
624        self.doc
625            .stylist
626            .force_stylesheet_origins_dirty(OriginSet::all());
627
628        self.doc.nodes_to_stylesheet.remove(&node_id);
629    }
630
631    fn load_image(&mut self, target_id: usize) {
632        let node = &self.doc.nodes[target_id];
633        if let Some(raw_src) = node.attr(local_name!("src")) {
634            if !raw_src.is_empty() {
635                let src = self.doc.resolve_url(raw_src);
636                self.doc.net_provider.fetch(
637                    self.doc.id(),
638                    Request::get(src),
639                    Box::new(ImageHandler::new(target_id, ImageType::Image)),
640                );
641            }
642        }
643    }
644
645    fn load_custom_paint_src(&mut self, target_id: usize) {
646        let node = &mut self.doc.nodes[target_id];
647        if let Some(raw_src) = node.attr(local_name!("src")) {
648            if let Ok(custom_paint_source_id) = raw_src.parse::<u64>() {
649                self.recompute_is_animating = true;
650                let canvas_data = SpecialElementData::Canvas(CanvasData {
651                    custom_paint_source_id,
652                });
653                node.element_data_mut().unwrap().special_data = canvas_data;
654            }
655        }
656    }
657
658    fn process_button_input(&mut self, target_id: usize) {
659        let node = &self.doc.nodes[target_id];
660        let Some(data) = node.element_data() else {
661            return;
662        };
663
664        let tagname = data.name.local.as_ref();
665        let type_attr = data.attr(local_name!("type"));
666        let value = data.attr(local_name!("value"));
667
668        // Add content of "value" attribute as a text node child if:
669        //   - Tag name is
670        if let ("input", Some("button" | "submit" | "reset"), Some(value)) =
671            (tagname, type_attr, value)
672        {
673            let value = value.to_string();
674            let id = self.create_text_node(&value);
675            self.append_children(target_id, &[id]);
676            return;
677        }
678        #[cfg(feature = "file_input")]
679        if let ("input", Some("file")) = (tagname, type_attr) {
680            let button_id = self.create_element(
681                qual_name!("button", html),
682                vec![
683                    Attribute {
684                        name: qual_name!("type", html),
685                        value: "button".to_string(),
686                    },
687                    Attribute {
688                        name: qual_name!("tabindex", html),
689                        value: "-1".to_string(),
690                    },
691                ],
692            );
693            let label_id = self.create_element(qual_name!("label", html), vec![]);
694            let text_id = self.create_text_node("No File Selected");
695            let button_text_id = self.create_text_node("Browse");
696            self.append_children(target_id, &[button_id, label_id]);
697            self.append_children(label_id, &[text_id]);
698            self.append_children(button_id, &[button_text_id]);
699        }
700    }
701}
702
703/// Set 'checked' state on an input based on given attributevalue
704fn set_input_checked_state(element: &mut ElementData, value: String) {
705    let Ok(checked) = value.parse() else {
706        return;
707    };
708    match element.special_data {
709        SpecialElementData::CheckboxInput(ref mut checked_mut) => *checked_mut = checked,
710        // If we have just constructed the element, set the node attribute,
711        // and NodeSpecificData will be created from that later
712        // this simulates the checked attribute being set in html,
713        // and the element's checked property being set from that
714        SpecialElementData::None => element.attrs.push(Attribute {
715            name: qual_name!("checked", html),
716            value: checked.to_string(),
717        }),
718        _ => {}
719    }
720}
721
722/// Type that allows mutable access to the viewport
723/// And syncs it back to stylist on drop.
724pub struct ViewportMut<'doc> {
725    doc: &'doc mut BaseDocument,
726    initial_scale: f64,
727}
728impl ViewportMut<'_> {
729    pub fn new(doc: &mut BaseDocument) -> ViewportMut<'_> {
730        let initial_scale = doc.viewport.scale_f64();
731        ViewportMut { doc, initial_scale }
732    }
733}
734impl Deref for ViewportMut<'_> {
735    type Target = Viewport;
736
737    fn deref(&self) -> &Self::Target {
738        &self.doc.viewport
739    }
740}
741impl DerefMut for ViewportMut<'_> {
742    fn deref_mut(&mut self) -> &mut Self::Target {
743        &mut self.doc.viewport
744    }
745}
746impl Drop for ViewportMut<'_> {
747    fn drop(&mut self) {
748        self.doc
749            .set_stylist_device(make_device(&self.doc.viewport, self.doc.font_ctx.clone()));
750        self.doc.scroll_viewport_by(0.0, 0.0); // Clamp scroll offset
751
752        let scale_has_changed = self.doc.viewport().scale_f64() != self.initial_scale;
753        if scale_has_changed {
754            self.doc.invalidate_inline_contexts();
755        }
756    }
757}