1use cssparser::ParserInput;
2use linebender_resource_handle::Blob;
3use markup5ever::{LocalName, QualName, local_name};
4use parley::{ContentWidths, FontContext, LayoutContext};
5use selectors::matching::QuirksMode;
6use std::str::FromStr;
7use std::sync::Arc;
8use style::Atom;
9use style::parser::ParserContext;
10use style::properties::{Importance, PropertyDeclaration, PropertyId, SourcePropertyDeclaration};
11use style::stylesheets::{DocumentStyleSheet, Origin, UrlExtraData};
12use style::{
13 properties::{PropertyDeclarationBlock, parse_style_attribute},
14 servo_arc::Arc as ServoArc,
15 shared_lock::{Locked, SharedRwLock},
16 stylesheets::CssRuleType,
17};
18use style_traits::ParsingMode;
19use url::Url;
20
21use super::{Attribute, Attributes};
22use crate::Document;
23use crate::layout::table::TableContext;
24
25#[cfg(feature = "custom-widget")]
26use super::custom_widget::CustomWidgetData;
27
28macro_rules! local_names {
29 ($($name:tt),+) => {
30 [$(local_name!($name),)+]
31 };
32}
33
34#[derive(Debug, Clone)]
35pub struct ElementData {
36 pub name: QualName,
38
39 pub id: Option<Atom>,
41
42 pub attrs: Attributes,
44
45 pub is_focussable: bool,
47
48 pub style_attribute: Option<ServoArc<Locked<PropertyDeclarationBlock>>>,
50
51 pub special_data: SpecialElementData,
57
58 pub background_images: Vec<Option<BackgroundImageData>>,
59
60 pub inline_layout_data: Option<Box<TextLayout>>,
62
63 pub list_item_data: Option<Box<ListItemLayout>>,
66
67 pub template_contents: Option<usize>,
69 }
72
73#[derive(Copy, Clone, Default)]
74#[non_exhaustive]
75pub enum SpecialElementType {
76 Stylesheet,
77 Image,
78 Canvas,
79 TableRoot,
80 TextInput,
81 CheckboxInput,
82 #[cfg(feature = "file_input")]
83 FileInput,
84 #[default]
85 None,
86}
87
88#[derive(Default)]
90pub enum SpecialElementData {
91 SubDocument(Box<dyn Document>),
93 #[cfg(feature = "custom-widget")]
95 CustomWidget(CustomWidgetData),
96 Stylesheet(DocumentStyleSheet),
98 Image(Box<ImageData>),
100 Canvas(CanvasData),
102 TableRoot(Arc<TableContext>),
104 TextInput(TextInputData),
106 CheckboxInput(bool),
108 #[cfg(feature = "file_input")]
110 FileInput(FileData),
111 #[default]
113 None,
114}
115
116impl Clone for SpecialElementData {
117 fn clone(&self) -> Self {
118 match self {
119 Self::SubDocument(_) => Self::None, #[cfg(feature = "custom-widget")]
121 Self::CustomWidget(_) => Self::None, Self::Stylesheet(data) => Self::Stylesheet(data.clone()),
123 Self::Image(data) => Self::Image(data.clone()),
124 Self::Canvas(data) => Self::Canvas(data.clone()),
125 Self::TableRoot(data) => Self::TableRoot(data.clone()),
126 Self::TextInput(data) => Self::TextInput(data.clone()),
127 Self::CheckboxInput(data) => Self::CheckboxInput(*data),
128 #[cfg(feature = "file_input")]
129 Self::FileInput(data) => Self::FileInput(data.clone()),
130 Self::None => Self::None,
131 }
132 }
133}
134
135impl SpecialElementData {
136 pub fn take(&mut self) -> Self {
137 std::mem::take(self)
138 }
139}
140
141impl ElementData {
142 pub fn new(name: QualName, attrs: Vec<Attribute>) -> Self {
143 let id_attr_atom = attrs
144 .iter()
145 .find(|attr| &attr.name.local == "id")
146 .map(|attr| attr.value.as_ref())
147 .map(|value: &str| Atom::from(value));
148
149 let mut data = ElementData {
150 name,
151 id: id_attr_atom,
152 attrs: Attributes::new(attrs),
153 is_focussable: false,
154 style_attribute: Default::default(),
155 inline_layout_data: None,
156 list_item_data: None,
157 special_data: SpecialElementData::None,
158 template_contents: None,
159 background_images: Vec::new(),
160 };
161 data.flush_is_focussable();
162 data
163 }
164
165 pub fn attrs(&self) -> &[Attribute] {
166 &self.attrs
167 }
168
169 pub fn attr(&self, name: impl PartialEq<LocalName>) -> Option<&str> {
170 let attr = self.attrs.iter().find(|attr| name == attr.name.local)?;
171 Some(&attr.value)
172 }
173
174 pub fn attr_parsed<T: FromStr>(&self, name: impl PartialEq<LocalName>) -> Option<T> {
175 let attr = self.attrs.iter().find(|attr| name == attr.name.local)?;
176 attr.value.parse::<T>().ok()
177 }
178
179 pub fn has_attr(&self, name: impl PartialEq<LocalName>) -> bool {
181 self.attrs.iter().any(|attr| name == attr.name.local)
182 }
183
184 pub fn can_be_disabled(&self) -> bool {
185 local_names!("button", "input", "select", "textarea").contains(&self.name.local)
186 }
187
188 pub fn image_data(&self) -> Option<&ImageData> {
189 match &self.special_data {
190 SpecialElementData::Image(data) => Some(&**data),
191 _ => None,
192 }
193 }
194
195 pub fn image_data_mut(&mut self) -> Option<&mut ImageData> {
196 match self.special_data {
197 SpecialElementData::Image(ref mut data) => Some(&mut **data),
198 _ => None,
199 }
200 }
201
202 pub fn raster_image_data(&self) -> Option<&RasterImageData> {
203 match self.image_data()? {
204 ImageData::Raster(data) => Some(data),
205 _ => None,
206 }
207 }
208
209 pub fn raster_image_data_mut(&mut self) -> Option<&mut RasterImageData> {
210 match self.image_data_mut()? {
211 ImageData::Raster(data) => Some(data),
212 _ => None,
213 }
214 }
215
216 pub fn canvas_data(&self) -> Option<&CanvasData> {
217 match &self.special_data {
218 SpecialElementData::Canvas(data) => Some(data),
219 _ => None,
220 }
221 }
222
223 pub fn sub_doc_data(&self) -> Option<&dyn Document> {
224 match &self.special_data {
225 SpecialElementData::SubDocument(data) => Some(data.as_ref()),
226 _ => None,
227 }
228 }
229
230 pub fn sub_doc_data_mut(&mut self) -> Option<&mut dyn Document> {
231 match &mut self.special_data {
232 SpecialElementData::SubDocument(data) => Some(data.as_mut()),
233 _ => None,
234 }
235 }
236
237 #[cfg(feature = "svg")]
238 pub fn svg_data(&self) -> Option<&usvg::Tree> {
239 match self.image_data()? {
240 ImageData::Svg(data) => Some(data),
241 _ => None,
242 }
243 }
244
245 pub fn text_input_data(&self) -> Option<&TextInputData> {
246 match &self.special_data {
247 SpecialElementData::TextInput(data) => Some(data),
248 _ => None,
249 }
250 }
251
252 pub fn text_input_data_mut(&mut self) -> Option<&mut TextInputData> {
253 match &mut self.special_data {
254 SpecialElementData::TextInput(data) => Some(data),
255 _ => None,
256 }
257 }
258
259 #[cfg(feature = "custom-widget")]
260 pub fn custom_widget_data(&self) -> Option<&CustomWidgetData> {
261 match &self.special_data {
262 SpecialElementData::CustomWidget(data) => Some(data),
263 _ => None,
264 }
265 }
266
267 #[cfg(feature = "custom-widget")]
268 pub fn custom_widget_data_mut(&mut self) -> Option<&mut CustomWidgetData> {
269 match &mut self.special_data {
270 SpecialElementData::CustomWidget(data) => Some(data),
271 _ => None,
272 }
273 }
274
275 pub fn checkbox_input_checked(&self) -> Option<bool> {
276 match self.special_data {
277 SpecialElementData::CheckboxInput(checked) => Some(checked),
278 _ => None,
279 }
280 }
281
282 pub fn checkbox_input_checked_mut(&mut self) -> Option<&mut bool> {
283 match self.special_data {
284 SpecialElementData::CheckboxInput(ref mut checked) => Some(checked),
285 _ => None,
286 }
287 }
288
289 #[cfg(feature = "file_input")]
290 pub fn file_data(&self) -> Option<&FileData> {
291 match &self.special_data {
292 SpecialElementData::FileInput(data) => Some(data),
293 _ => None,
294 }
295 }
296
297 #[cfg(feature = "file_input")]
298 pub fn file_data_mut(&mut self) -> Option<&mut FileData> {
299 match &mut self.special_data {
300 SpecialElementData::FileInput(data) => Some(data),
301 _ => None,
302 }
303 }
304
305 pub fn flush_is_focussable(&mut self) {
306 let disabled: bool = self.attr_parsed(local_name!("disabled")).unwrap_or(false);
307 let tabindex: Option<i32> = self.attr_parsed(local_name!("tabindex"));
308 let contains_sub_document: bool = self.sub_doc_data().is_some();
309
310 self.is_focussable = contains_sub_document
311 || (!disabled
312 && match tabindex {
313 Some(index) => index >= 0,
314 None => {
315 if [local_name!("a"), local_name!("area")].contains(&self.name.local) {
322 self.attr(local_name!("href")).is_some()
323 } else {
324 const DEFAULT_FOCUSSABLE_ELEMENTS: [LocalName; 6] = [
325 local_name!("button"),
326 local_name!("input"),
327 local_name!("select"),
328 local_name!("textarea"),
329 local_name!("frame"),
330 local_name!("iframe"),
331 ];
332 DEFAULT_FOCUSSABLE_ELEMENTS.contains(&self.name.local)
333 }
334 }
335 })
336 }
337
338 pub fn flush_style_attribute(&mut self, guard: &SharedRwLock, url_extra_data: &UrlExtraData) {
339 self.style_attribute = self.attr(local_name!("style")).map(|style_str| {
340 ServoArc::new(guard.wrap(parse_style_attribute(
341 style_str,
342 url_extra_data,
343 None,
344 QuirksMode::NoQuirks,
345 CssRuleType::Style,
346 )))
347 });
348 }
349
350 pub fn set_style_property(
351 &mut self,
352 name: &str,
353 value: &str,
354 guard: &SharedRwLock,
355 url_extra_data: UrlExtraData,
356 ) -> bool {
357 let context = ParserContext::new(
358 Origin::Author,
359 &url_extra_data,
360 Some(CssRuleType::Style),
361 ParsingMode::DEFAULT,
362 QuirksMode::NoQuirks,
363 Default::default(),
364 None,
365 None,
366 Default::default(),
367 );
368
369 let Ok(property_id) = PropertyId::parse(name, &context) else {
370 #[cfg(feature = "tracing")]
371 tracing::warn!(property = name, "Unsupported property");
372 return false;
373 };
374 let mut source_property_declaration = SourcePropertyDeclaration::default();
375 let mut input = ParserInput::new(value);
376 let mut parser = style::values::Parser::new(&mut input);
377 let Ok(_) = PropertyDeclaration::parse_into(
378 &mut source_property_declaration,
379 property_id,
380 &context,
381 &mut parser,
382 ) else {
383 #[cfg(feature = "tracing")]
384 tracing::warn!(property = name, value, "Invalid property value");
385 return false;
386 };
387
388 if self.style_attribute.is_none() {
389 self.style_attribute = Some(ServoArc::new(guard.wrap(PropertyDeclarationBlock::new())));
390 }
391 self.style_attribute
392 .as_mut()
393 .unwrap()
394 .write_with(&mut guard.write())
395 .extend(source_property_declaration.drain(), Importance::Normal);
396
397 true
398 }
399
400 pub fn remove_style_property(
401 &mut self,
402 name: &str,
403 guard: &SharedRwLock,
404 url_extra_data: UrlExtraData,
405 ) -> bool {
406 let context = ParserContext::new(
407 Origin::Author,
408 &url_extra_data,
409 Some(CssRuleType::Style),
410 ParsingMode::DEFAULT,
411 QuirksMode::NoQuirks,
412 Default::default(),
413 None,
414 None,
415 Default::default(),
416 );
417 let Ok(property_id) = PropertyId::parse(name, &context) else {
418 #[cfg(feature = "tracing")]
419 tracing::warn!(property = name, "Unsupported property");
420 return false;
421 };
422
423 if let Some(style) = &mut self.style_attribute {
424 let mut guard = guard.write();
425 let style = style.write_with(&mut guard);
426 if let Some(index) = style.first_declaration_to_remove(&property_id) {
427 style.remove_property(&property_id, index);
428 return true;
429 }
430 }
431
432 false
433 }
434
435 pub fn set_sub_document(&mut self, sub_document: Box<dyn Document>) {
436 self.special_data = SpecialElementData::SubDocument(sub_document);
437 }
438
439 pub fn remove_sub_document(&mut self) {
440 self.special_data = SpecialElementData::None;
441 }
442
443 #[cfg(feature = "custom-widget")]
444 pub fn set_custom_widget(&mut self, widget: Box<dyn crate::Widget>) {
445 use crate::node::custom_widget::CustomWidgetData;
446 self.special_data = SpecialElementData::CustomWidget(CustomWidgetData::new(widget));
447 }
448
449 #[cfg(feature = "custom-widget")]
450 pub fn remove_custom_widget(&mut self) -> Vec<anyrender::ResourceId> {
451 let resource_ids = self
452 .custom_widget_data_mut()
453 .map(|widget_data| widget_data.take_resource_ids())
454 .unwrap_or_default();
455 self.special_data = SpecialElementData::None;
456 resource_ids
457 }
458
459 pub fn take_inline_layout(&mut self) -> Option<Box<TextLayout>> {
460 std::mem::take(&mut self.inline_layout_data)
461 }
462
463 pub fn is_submit_button(&self) -> bool {
464 if self.name.local != local_name!("button") {
465 return false;
466 }
467 let type_attr = self.attr(local_name!("type"));
468 let is_submit = type_attr == Some("submit");
469 let is_auto_submit = type_attr.is_none()
470 && self.attr(LocalName::from("command")).is_none()
471 && self.attr(LocalName::from("commandfor")).is_none();
472 is_submit || is_auto_submit
473 }
474}
475
476#[derive(Debug, Clone, PartialEq)]
477pub struct RasterImageData {
478 pub width: u32,
480 pub height: u32,
482 pub data: Blob<u8>,
484}
485impl RasterImageData {
486 pub fn new(width: u32, height: u32, data: Arc<Vec<u8>>) -> Self {
487 Self {
488 width,
489 height,
490 data: Blob::new(data),
491 }
492 }
493}
494
495#[derive(Debug, Clone)]
496pub enum ImageData {
497 Raster(RasterImageData),
498 #[cfg(feature = "svg")]
499 Svg(Arc<usvg::Tree>),
500 None,
501}
502#[cfg(feature = "svg")]
503impl From<usvg::Tree> for ImageData {
504 fn from(value: usvg::Tree) -> Self {
505 Self::Svg(Arc::new(value))
506 }
507}
508
509#[derive(Debug, Clone, PartialEq)]
510pub enum Status {
511 Ok,
512 Error,
513 Loading,
514}
515
516#[derive(Debug, Clone)]
517pub struct BackgroundImageData {
518 pub url: ServoArc<Url>,
520 pub status: Status,
522 pub image: ImageData,
524}
525
526impl BackgroundImageData {
527 pub fn new(url: ServoArc<Url>) -> Self {
528 Self {
529 url,
530 status: Status::Loading,
531 image: ImageData::None,
532 }
533 }
534}
535
536pub struct TextInputData {
537 pub editor: Box<parley::PlainEditor<TextBrush>>,
539 pub is_multiline: bool,
541}
542
543impl Clone for TextInputData {
545 fn clone(&self) -> Self {
546 TextInputData::new(self.is_multiline)
547 }
548}
549
550impl TextInputData {
551 pub fn new(is_multiline: bool) -> Self {
552 let editor = Box::new(parley::PlainEditor::new(16.0));
553 Self {
554 editor,
555 is_multiline,
556 }
557 }
558
559 pub fn set_text(
560 &mut self,
561 font_ctx: &mut FontContext,
562 layout_ctx: &mut LayoutContext<TextBrush>,
563 text: &str,
564 ) {
565 if self.editor.text() != text {
566 self.editor.set_text(text);
567 self.editor.driver(font_ctx, layout_ctx).refresh_layout();
568 }
569 }
570}
571
572#[derive(Debug, Clone)]
573pub struct CanvasData {
574 pub custom_paint_source_id: u64,
575}
576
577impl std::fmt::Debug for SpecialElementData {
578 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
579 match self {
580 SpecialElementData::SubDocument(_) => f.write_str("NodeSpecificData::SubDocument"),
581 #[cfg(feature = "custom-widget")]
582 SpecialElementData::CustomWidget(_) => f.write_str("NodeSpecificData::CustomWidget"),
583 SpecialElementData::Stylesheet(_) => f.write_str("NodeSpecificData::Stylesheet"),
584 SpecialElementData::Image(data) => match **data {
585 ImageData::Raster(_) => f.write_str("NodeSpecificData::Image(Raster)"),
586 #[cfg(feature = "svg")]
587 ImageData::Svg(_) => f.write_str("NodeSpecificData::Image(Svg)"),
588 ImageData::None => f.write_str("NodeSpecificData::Image(None)"),
589 },
590 SpecialElementData::Canvas(_) => f.write_str("NodeSpecificData::Canvas"),
591 SpecialElementData::TableRoot(_) => f.write_str("NodeSpecificData::TableRoot"),
592 SpecialElementData::TextInput(_) => f.write_str("NodeSpecificData::TextInput"),
593 SpecialElementData::CheckboxInput(_) => f.write_str("NodeSpecificData::CheckboxInput"),
594 #[cfg(feature = "file_input")]
595 SpecialElementData::FileInput(_) => f.write_str("NodeSpecificData::FileInput"),
596 SpecialElementData::None => f.write_str("NodeSpecificData::None"),
597 }
598 }
599}
600
601#[derive(Clone)]
602pub struct ListItemLayout {
603 pub marker: Marker,
604 pub position: ListItemLayoutPosition,
605}
606
607#[derive(Debug, PartialEq, Clone)]
610pub enum Marker {
611 Char(char),
612 String(String),
613}
614
615#[derive(Clone)]
617pub enum ListItemLayoutPosition {
618 Inside,
619 Outside(Box<parley::Layout<TextBrush>>),
620}
621
622impl std::fmt::Debug for ListItemLayout {
623 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
624 write!(f, "ListItemLayout - marker {:?}", self.marker)
625 }
626}
627
628#[derive(Debug, Clone, Copy, Default, PartialEq)]
629pub struct TextBrush {
631 pub id: usize,
633}
634
635impl TextBrush {
636 pub(crate) fn from_id(id: usize) -> Self {
637 Self { id }
638 }
639}
640
641#[derive(Clone, Default)]
642pub struct TextLayout {
643 pub text: String,
644 pub content_widths: Option<ContentWidths>,
645 pub layout: parley::layout::Layout<TextBrush>,
646}
647
648impl TextLayout {
649 pub fn new() -> Self {
650 Default::default()
651 }
652
653 pub fn content_widths(&mut self) -> ContentWidths {
654 *self
655 .content_widths
656 .get_or_insert_with(|| self.layout.calculate_content_widths())
657 }
658}
659
660impl std::fmt::Debug for TextLayout {
661 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
662 write!(f, "TextLayout")
663 }
664}
665
666#[cfg(feature = "file_input")]
667mod file_data {
668 use std::ops::{Deref, DerefMut};
669 use std::path::PathBuf;
670
671 #[derive(Clone, Debug)]
672 pub struct FileData(pub Vec<PathBuf>);
673 impl Deref for FileData {
674 type Target = Vec<PathBuf>;
675
676 fn deref(&self) -> &Self::Target {
677 &self.0
678 }
679 }
680 impl DerefMut for FileData {
681 fn deref_mut(&mut self) -> &mut Self::Target {
682 &mut self.0
683 }
684 }
685 impl From<Vec<PathBuf>> for FileData {
686 fn from(files: Vec<PathBuf>) -> Self {
687 Self(files)
688 }
689 }
690}
691#[cfg(feature = "file_input")]
692pub use file_data::FileData;