Skip to main content

script/
drag_data_store.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::sync::Arc;
6
7use indexmap::IndexMap;
8use js::context::JSContext;
9use pixels::RasterImage;
10use servo_constellation_traits::BlobImpl;
11
12use crate::dom::bindings::error::{Error, Fallible};
13use crate::dom::bindings::root::DomRoot;
14use crate::dom::bindings::str::DOMString;
15use crate::dom::file::File;
16use crate::dom::globalscope::GlobalScope;
17use crate::script_runtime::CanGc;
18
19/// <https://html.spec.whatwg.org/multipage/#the-drag-data-item-kind>
20#[derive(MallocSizeOf)]
21pub(crate) enum Kind {
22    Text {
23        data: DOMString,
24        type_: DOMString,
25    },
26    File {
27        bytes: Vec<u8>,
28        name: DOMString,
29        type_: String,
30    },
31}
32
33impl Kind {
34    pub(crate) fn type_(&self) -> DOMString {
35        match self {
36            Kind::Text { type_, .. } => type_.clone(),
37            Kind::File { type_, .. } => DOMString::from(type_.clone()),
38        }
39    }
40
41    pub(crate) fn as_string(&self) -> Option<String> {
42        match self {
43            Kind::Text { data, .. } => Some(data.to_string()),
44            Kind::File { .. } => None,
45        }
46    }
47
48    // TODO for now we create a new BlobImpl
49    // since File constructor requires moving it.
50    pub(crate) fn as_file(
51        &self,
52        cx: &mut JSContext,
53        global: &GlobalScope,
54    ) -> Option<DomRoot<File>> {
55        match self {
56            Kind::Text { .. } => None,
57            Kind::File { bytes, name, type_ } => Some(File::new(
58                global,
59                BlobImpl::new_from_bytes(bytes.clone(), type_.clone()),
60                name.clone(),
61                None,
62                CanGc::from_cx(cx),
63            )),
64        }
65    }
66
67    fn text_type_matches(&self, text_type: &DOMString) -> bool {
68        matches!(self, Kind::Text { type_, .. } if type_.eq(text_type))
69    }
70
71    fn is_file(&self) -> bool {
72        matches!(self, Kind::File { .. })
73    }
74}
75
76/// <https://html.spec.whatwg.org/multipage/#drag-data-store-bitmap>
77#[derive(MallocSizeOf)]
78struct Bitmap {
79    #[conditional_malloc_size_of]
80    image: Option<Arc<RasterImage>>,
81    x: i32,
82    y: i32,
83}
84
85/// Control the behaviour of the drag data store
86#[derive(Clone, Copy, Eq, MallocSizeOf, PartialEq)]
87pub(crate) enum Mode {
88    /// <https://html.spec.whatwg.org/multipage/#concept-dnd-rw>
89    ReadWrite,
90    /// <https://html.spec.whatwg.org/multipage/#concept-dnd-ro>
91    ReadOnly,
92    /// <https://html.spec.whatwg.org/multipage/#concept-dnd-p>
93    Protected,
94}
95
96#[derive(MallocSizeOf)]
97pub(crate) struct DragDataStore {
98    /// <https://html.spec.whatwg.org/multipage/#drag-data-store-item-list>
99    item_list: IndexMap<u16, Kind>,
100    next_item_id: u16,
101    /// <https://html.spec.whatwg.org/multipage/#drag-data-store-default-feedback>
102    default_feedback: Option<String>,
103    bitmap: Option<Bitmap>,
104    mode: Mode,
105    /// <https://html.spec.whatwg.org/multipage/#drag-data-store-allowed-effects-state>
106    allowed_effects_state: String,
107    pub clear_was_called: bool,
108}
109
110impl DragDataStore {
111    /// <https://html.spec.whatwg.org/multipage/#create-a-drag-data-store>
112    // We don't really need it since it's only instantiated by DataTransfer.
113    pub(crate) fn new() -> DragDataStore {
114        DragDataStore {
115            item_list: IndexMap::new(),
116            next_item_id: 0,
117            default_feedback: None,
118            bitmap: None,
119            mode: Mode::Protected,
120            allowed_effects_state: String::from("uninitialized"),
121            clear_was_called: false,
122        }
123    }
124
125    /// Get the drag data store mode
126    pub(crate) fn mode(&self) -> Mode {
127        self.mode
128    }
129
130    /// Set the drag data store mode
131    pub(crate) fn set_mode(&mut self, mode: Mode) {
132        self.mode = mode;
133    }
134
135    pub(crate) fn set_bitmap(&mut self, image: Option<Arc<RasterImage>>, x: i32, y: i32) {
136        self.bitmap = Some(Bitmap { image, x, y });
137    }
138
139    /// <https://html.spec.whatwg.org/multipage/#concept-datatransfer-types>
140    pub(crate) fn types(&self) -> Vec<DOMString> {
141        let mut types = Vec::new();
142
143        let has_files = self.item_list.values().fold(false, |has_files, item| {
144            // Step 2.1 For each item in the item list whose kind is text,
145            // add an entry to L consisting of the item's type string.
146            match item {
147                Kind::Text { type_, .. } => types.push(type_.clone()),
148                Kind::File { .. } => return true,
149            }
150
151            has_files
152        });
153
154        // Step 2.2 If there are any items in the item list whose kind is File,
155        // add an entry to L consisting of the string "Files".
156        if has_files {
157            types.push(DOMString::from("Files"));
158        }
159        types
160    }
161
162    pub(crate) fn find_matching_text(&self, type_: &DOMString) -> Option<DOMString> {
163        self.item_list
164            .values()
165            .find(|item| item.text_type_matches(type_))
166            .and_then(|item| item.as_string())
167            .map(DOMString::from)
168    }
169
170    pub(crate) fn add(&mut self, kind: Kind) -> Fallible<u16> {
171        if let Kind::Text { ref type_, .. } = kind {
172            // Step 2.1 If there is already an item in the item list whose kind is text
173            // and whose type string is equal to the method's second argument, throw "NotSupportedError".
174            if self
175                .item_list
176                .values()
177                .any(|item| item.text_type_matches(type_))
178            {
179                return Err(Error::NotSupported(None));
180            }
181        }
182
183        let item_id = self.next_item_id;
184
185        // Step 2.2
186        self.item_list.insert(item_id, kind);
187
188        self.next_item_id += 1;
189        Ok(item_id)
190    }
191
192    pub(crate) fn set_data(&mut self, format: DOMString, data: DOMString) {
193        // Step 3-4
194        let type_ = normalize_mime(format);
195
196        // Step 5 Remove the item in the drag data store item list whose kind is text
197        // and whose type string is equal to format, if there is one.
198        self.item_list
199            .retain(|_, item| !item.text_type_matches(&type_));
200
201        // Step 6 Add an item whose kind is text, whose type is format, and whose data is the method's second argument.
202        self.item_list
203            .insert(self.next_item_id, Kind::Text { data, type_ });
204        self.next_item_id += 1;
205    }
206
207    pub(crate) fn clear_data(&mut self, format: Option<DOMString>) -> bool {
208        let mut was_modified = false;
209
210        if let Some(format) = format {
211            // Step 4-5
212            let type_ = normalize_mime(format);
213
214            // Step 6 Remove the item in the item list whose kind is text and whose type is format.
215            self.item_list.retain(|_, item| {
216                let matches = item.text_type_matches(&type_);
217
218                if matches {
219                    was_modified = true;
220                }
221                !matches
222            });
223        } else {
224            // Step 3 Remove each item in the item list whose kind is text.
225            self.item_list.retain(|_, item| {
226                let matches = item.is_file();
227
228                if !matches {
229                    was_modified = true;
230                }
231                matches
232            });
233        }
234
235        was_modified
236    }
237
238    pub(crate) fn files(
239        &self,
240        cx: &mut JSContext,
241        global: &GlobalScope,
242        file_list: &mut Vec<DomRoot<File>>,
243    ) {
244        // Step 3 If the data store is in the protected mode return the empty list.
245        if self.mode == Mode::Protected {
246            return;
247        }
248
249        // Step 4 For each item in the drag data store item list whose kind is File, add the item's data to the list L.
250        self.item_list
251            .values()
252            .filter_map(|item| item.as_file(cx, global))
253            .for_each(|file| file_list.push(file));
254    }
255
256    pub(crate) fn list_len(&self) -> usize {
257        self.item_list.len()
258    }
259
260    pub(crate) fn iter_item_list(&self) -> indexmap::map::Values<'_, u16, Kind> {
261        self.item_list.values()
262    }
263
264    pub(crate) fn get_by_index(&self, index: usize) -> Option<(&u16, &Kind)> {
265        self.item_list.get_index(index)
266    }
267
268    pub(crate) fn get_by_id(&self, id: &u16) -> Option<&Kind> {
269        self.item_list.get(id)
270    }
271
272    pub(crate) fn remove(&mut self, index: usize) {
273        self.item_list.shift_remove_index(index);
274    }
275
276    pub(crate) fn clear_list(&mut self) {
277        self.item_list.clear();
278        self.clear_was_called = true;
279    }
280}
281
282fn normalize_mime(mut format: DOMString) -> DOMString {
283    // Convert format to ASCII lowercase.
284    format.make_ascii_lowercase();
285
286    match &*format.str() {
287        // If format equals "text", change it to "text/plain".
288        "text" => DOMString::from("text/plain"),
289        // If format equals "url", change it to "text/uri-list".
290        "url" => DOMString::from("text/uri-list"),
291        s => DOMString::from(s),
292    }
293}