1#![deny(unsafe_code)]
10
11mod layout_damage;
12pub mod wrapper_traits;
13
14use std::any::Any;
15use std::rc::Rc;
16use std::sync::Arc;
17use std::sync::atomic::{AtomicIsize, AtomicU64, Ordering};
18use std::thread::JoinHandle;
19use std::time::Duration;
20
21use app_units::Au;
22use background_hang_monitor_api::BackgroundHangMonitorRegister;
23use base::Epoch;
24use base::generic_channel::GenericSender;
25use base::id::{BrowsingContextId, PipelineId, WebViewId};
26use bitflags::bitflags;
27use embedder_traits::{Cursor, Theme, UntrustedNodeAddress, ViewportDetails};
28use euclid::{Point2D, Rect};
29use fonts::{FontContext, WebFontDocumentContext};
30pub use layout_damage::LayoutDamage;
31use libc::c_void;
32use malloc_size_of::{MallocSizeOf as MallocSizeOfTrait, MallocSizeOfOps, malloc_size_of_is_0};
33use malloc_size_of_derive::MallocSizeOf;
34use net_traits::image_cache::{ImageCache, ImageCacheFactory, PendingImageId};
35use paint_api::CrossProcessPaintApi;
36use parking_lot::RwLock;
37use pixels::RasterImage;
38use profile_traits::mem::Report;
39use profile_traits::time;
40use rustc_hash::FxHashMap;
41use script_traits::{InitialScriptState, Painter, ScriptThreadMessage};
42use serde::{Deserialize, Serialize};
43use servo_arc::Arc as ServoArc;
44use servo_url::{ImmutableOrigin, ServoUrl};
45use style::Atom;
46use style::animation::DocumentAnimationSet;
47use style::attr::{AttrValue, parse_integer, parse_unsigned_integer};
48use style::context::QuirksMode;
49use style::data::ElementDataWrapper;
50use style::device::Device;
51use style::dom::OpaqueNode;
52use style::invalidation::element::restyle_hints::RestyleHint;
53use style::properties::style_structs::Font;
54use style::properties::{ComputedValues, PropertyId};
55use style::selector_parser::{PseudoElement, RestyleDamage, Snapshot};
56use style::str::char_is_whitespace;
57use style::stylesheets::{DocumentStyleSheet, Stylesheet};
58use style::stylist::Stylist;
59use style::thread_state::{self, ThreadState};
60use style::values::computed::Overflow;
61use style_traits::CSSPixel;
62use webrender_api::units::{DeviceIntSize, LayoutPoint, LayoutVector2D};
63use webrender_api::{ExternalScrollId, ImageKey};
64
65pub trait GenericLayoutDataTrait: Any + MallocSizeOfTrait {
66 fn as_any(&self) -> &dyn Any;
67}
68
69pub type GenericLayoutData = dyn GenericLayoutDataTrait + Send + Sync;
70
71#[derive(Default, MallocSizeOf)]
72pub struct StyleData {
73 pub element_data: ElementDataWrapper,
78
79 pub parallel: DomParallelInfo,
81}
82
83#[derive(Default, MallocSizeOf)]
85pub struct DomParallelInfo {
86 pub children_to_process: AtomicIsize,
88}
89
90#[derive(Clone, Copy, Debug, Eq, PartialEq)]
91pub enum LayoutNodeType {
92 Element(LayoutElementType),
93 Text,
94}
95
96#[derive(Clone, Copy, Debug, Eq, PartialEq)]
97pub enum LayoutElementType {
98 Element,
99 HTMLBodyElement,
100 HTMLBRElement,
101 HTMLCanvasElement,
102 HTMLHtmlElement,
103 HTMLIFrameElement,
104 HTMLImageElement,
105 HTMLInputElement,
106 HTMLMediaElement,
107 HTMLObjectElement,
108 HTMLOptGroupElement,
109 HTMLOptionElement,
110 HTMLParagraphElement,
111 HTMLPreElement,
112 HTMLSelectElement,
113 HTMLTableCellElement,
114 HTMLTableColElement,
115 HTMLTableElement,
116 HTMLTableRowElement,
117 HTMLTableSectionElement,
118 HTMLTextAreaElement,
119 SVGImageElement,
120 SVGSVGElement,
121}
122
123pub struct HTMLCanvasData {
124 pub image_key: Option<ImageKey>,
125 pub width: u32,
126 pub height: u32,
127}
128
129pub struct SVGElementData<'dom> {
130 pub source: Option<Result<ServoUrl, ()>>,
132 pub width: Option<&'dom AttrValue>,
133 pub height: Option<&'dom AttrValue>,
134 pub svg_id: String,
135 pub view_box: Option<&'dom AttrValue>,
136}
137
138impl SVGElementData<'_> {
139 pub fn ratio_from_view_box(&self) -> Option<f32> {
140 let mut iter = self.view_box?.chars();
141 let _min_x = parse_integer(&mut iter).ok()?;
142 let _min_y = parse_integer(&mut iter).ok()?;
143
144 let width = parse_unsigned_integer(&mut iter).ok()?;
145 if width == 0 {
146 return None;
147 }
148
149 let height = parse_unsigned_integer(&mut iter).ok()?;
150 if height == 0 {
151 return None;
152 }
153
154 let mut iter = iter.skip_while(|c| char_is_whitespace(*c));
155 iter.next().is_none().then(|| width as f32 / height as f32)
156 }
157}
158
159#[derive(Clone, Copy, Debug, Eq, PartialEq)]
161pub struct TrustedNodeAddress(pub *const c_void);
162
163#[expect(unsafe_code)]
164unsafe impl Send for TrustedNodeAddress {}
165
166#[derive(Debug)]
168pub enum PendingImageState {
169 Unrequested(ServoUrl),
170 PendingResponse,
171}
172
173#[derive(Debug, MallocSizeOf)]
175pub enum LayoutImageDestination {
176 BoxTreeConstruction,
177 DisplayListBuilding,
178}
179
180#[derive(Debug)]
184pub struct PendingImage {
185 pub state: PendingImageState,
186 pub node: UntrustedNodeAddress,
187 pub id: PendingImageId,
188 pub origin: ImmutableOrigin,
189 pub destination: LayoutImageDestination,
190}
191
192#[derive(Debug)]
196pub struct PendingRasterizationImage {
197 pub node: UntrustedNodeAddress,
198 pub id: PendingImageId,
199 pub size: DeviceIntSize,
200}
201
202#[derive(Clone, Copy, Debug, MallocSizeOf)]
203pub struct MediaFrame {
204 pub image_key: webrender_api::ImageKey,
205 pub width: i32,
206 pub height: i32,
207}
208
209pub struct MediaMetadata {
210 pub width: u32,
211 pub height: u32,
212}
213
214pub struct HTMLMediaData {
215 pub current_frame: Option<MediaFrame>,
216 pub metadata: Option<MediaMetadata>,
217}
218
219pub struct LayoutConfig {
220 pub id: PipelineId,
221 pub webview_id: WebViewId,
222 pub url: ServoUrl,
223 pub is_iframe: bool,
224 pub script_chan: GenericSender<ScriptThreadMessage>,
225 pub image_cache: Arc<dyn ImageCache>,
226 pub font_context: Arc<FontContext>,
227 pub time_profiler_chan: time::ProfilerChan,
228 pub paint_api: CrossProcessPaintApi,
229 pub viewport_details: ViewportDetails,
230 pub user_stylesheets: Rc<Vec<DocumentStyleSheet>>,
231 pub theme: Theme,
232 pub accessibility_active: bool,
233}
234
235pub trait LayoutFactory: Send + Sync {
236 fn create(&self, config: LayoutConfig) -> Box<dyn Layout>;
237}
238
239pub trait Layout {
240 fn device(&self) -> &Device;
243
244 fn set_theme(&mut self, theme: Theme) -> bool;
248
249 fn set_viewport_details(&mut self, viewport_details: ViewportDetails) -> bool;
253
254 fn load_web_fonts_from_stylesheet(
257 &self,
258 stylesheet: &ServoArc<Stylesheet>,
259 font_context: &WebFontDocumentContext,
260 );
261
262 fn add_stylesheet(
266 &mut self,
267 stylesheet: ServoArc<Stylesheet>,
268 before_stylsheet: Option<ServoArc<Stylesheet>>,
269 font_context: &WebFontDocumentContext,
270 );
271
272 fn exit_now(&mut self);
274
275 fn collect_reports(&self, reports: &mut Vec<Report>, ops: &mut MallocSizeOfOps);
278
279 fn set_quirks_mode(&mut self, quirks_mode: QuirksMode);
281
282 fn remove_stylesheet(&mut self, stylesheet: ServoArc<Stylesheet>);
284
285 fn remove_cached_image(&mut self, image_url: &ServoUrl);
287
288 fn reflow(&mut self, reflow_request: ReflowRequest) -> Option<ReflowResult>;
290
291 fn ensure_stacking_context_tree(&self, viewport_details: ViewportDetails);
294
295 fn register_paint_worklet_modules(
297 &mut self,
298 name: Atom,
299 properties: Vec<Atom>,
300 painter: Box<dyn Painter>,
301 );
302
303 fn set_scroll_offsets_from_renderer(
305 &mut self,
306 scroll_states: &FxHashMap<ExternalScrollId, LayoutVector2D>,
307 );
308
309 fn scroll_offset(&self, id: ExternalScrollId) -> Option<LayoutVector2D>;
312
313 fn needs_new_display_list(&self) -> bool;
315
316 fn set_needs_new_display_list(&self);
318
319 fn query_padding(&self, node: TrustedNodeAddress) -> Option<PhysicalSides>;
320 fn query_box_area(
321 &self,
322 node: TrustedNodeAddress,
323 area: BoxAreaType,
324 exclude_transform_and_inline: bool,
325 ) -> Option<Rect<Au, CSSPixel>>;
326 fn query_box_areas(&self, node: TrustedNodeAddress, area: BoxAreaType) -> CSSPixelRectIterator;
327 fn query_client_rect(&self, node: TrustedNodeAddress) -> Rect<i32, CSSPixel>;
328 fn query_current_css_zoom(&self, node: TrustedNodeAddress) -> f32;
329 fn query_element_inner_outer_text(&self, node: TrustedNodeAddress) -> String;
330 fn query_offset_parent(&self, node: TrustedNodeAddress) -> OffsetParentResponse;
331 fn query_scroll_container(
334 &self,
335 node: Option<TrustedNodeAddress>,
336 flags: ScrollContainerQueryFlags,
337 ) -> Option<ScrollContainerResponse>;
338 fn query_resolved_style(
339 &self,
340 node: TrustedNodeAddress,
341 pseudo: Option<PseudoElement>,
342 property_id: PropertyId,
343 animations: DocumentAnimationSet,
344 animation_timeline_value: f64,
345 ) -> String;
346 fn query_resolved_font_style(
347 &self,
348 node: TrustedNodeAddress,
349 value: &str,
350 animations: DocumentAnimationSet,
351 animation_timeline_value: f64,
352 ) -> Option<ServoArc<Font>>;
353 fn query_scrolling_area(&self, node: Option<TrustedNodeAddress>) -> Rect<i32, CSSPixel>;
354 fn query_text_index(
356 &self,
357 node: TrustedNodeAddress,
358 point: Point2D<Au, CSSPixel>,
359 ) -> Option<usize>;
360 fn query_elements_from_point(
361 &self,
362 point: LayoutPoint,
363 flags: ElementsFromPointFlags,
364 ) -> Vec<ElementsFromPointResult>;
365 fn query_effective_overflow(&self, node: TrustedNodeAddress) -> Option<AxesOverflow>;
366 fn stylist_mut(&mut self) -> &mut Stylist;
367
368 fn set_accessibility_active(&self, active: bool);
369}
370
371pub trait ScriptThreadFactory {
375 fn create(
377 state: InitialScriptState,
378 layout_factory: Arc<dyn LayoutFactory>,
379 image_cache_factory: Arc<dyn ImageCacheFactory>,
380 background_hang_monitor_register: Box<dyn BackgroundHangMonitorRegister>,
381 ) -> JoinHandle<()>;
382}
383
384#[derive(Copy, Clone)]
387pub enum BoxAreaType {
388 Content,
389 Padding,
390 Border,
391}
392
393pub type CSSPixelRectIterator = Box<dyn Iterator<Item = Rect<Au, CSSPixel>>>;
394
395#[derive(Default)]
396pub struct PhysicalSides {
397 pub left: Au,
398 pub top: Au,
399 pub right: Au,
400 pub bottom: Au,
401}
402
403#[derive(Clone, Default)]
404pub struct OffsetParentResponse {
405 pub node_address: Option<UntrustedNodeAddress>,
406 pub rect: Rect<Au, CSSPixel>,
407}
408
409bitflags! {
410 #[derive(PartialEq)]
411 pub struct ScrollContainerQueryFlags: u8 {
412 const ForScrollParent = 1 << 0;
414 const Inclusive = 1 << 1;
416 }
417}
418
419#[derive(Clone, Copy, Debug, MallocSizeOf)]
420pub struct AxesOverflow {
421 pub x: Overflow,
422 pub y: Overflow,
423}
424
425impl Default for AxesOverflow {
426 fn default() -> Self {
427 Self {
428 x: Overflow::Visible,
429 y: Overflow::Visible,
430 }
431 }
432}
433
434impl From<&ComputedValues> for AxesOverflow {
435 fn from(style: &ComputedValues) -> Self {
436 Self {
437 x: style.clone_overflow_x(),
438 y: style.clone_overflow_y(),
439 }
440 }
441}
442
443impl AxesOverflow {
444 pub fn to_scrollable(&self) -> Self {
445 Self {
446 x: self.x.to_scrollable(),
447 y: self.y.to_scrollable(),
448 }
449 }
450
451 pub fn establishes_scroll_container(&self) -> bool {
453 self.x.is_scrollable()
456 }
457}
458
459#[derive(Clone)]
460pub enum ScrollContainerResponse {
461 Viewport(AxesOverflow),
462 Element(UntrustedNodeAddress, AxesOverflow),
463}
464
465#[derive(Debug, PartialEq)]
466pub enum QueryMsg {
467 BoxArea,
468 BoxAreas,
469 ClientRectQuery,
470 CurrentCSSZoomQuery,
471 EffectiveOverflow,
472 ElementInnerOuterTextQuery,
473 ElementsFromPoint,
474 InnerWindowDimensionsQuery,
475 NodesFromPointQuery,
476 OffsetParentQuery,
477 ScrollParentQuery,
478 ResolvedFontStyleQuery,
479 ResolvedStyleQuery,
480 ScrollingAreaOrOffsetQuery,
481 StyleQuery,
482 TextIndexQuery,
483 PaddingQuery,
484}
485
486#[derive(Debug, PartialEq)]
492pub enum ReflowGoal {
493 UpdateTheRendering,
496
497 LayoutQuery(QueryMsg),
500
501 UpdateScrollNode(ExternalScrollId, LayoutVector2D),
505}
506
507#[derive(Clone, Debug, MallocSizeOf)]
508pub struct IFrameSize {
509 pub browsing_context_id: BrowsingContextId,
510 pub pipeline_id: PipelineId,
511 pub viewport_details: ViewportDetails,
512}
513
514pub type IFrameSizes = FxHashMap<BrowsingContextId, IFrameSize>;
515
516bitflags! {
517 #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
520 pub struct RestyleReason: u16 {
521 const StylesheetsChanged = 1 << 0;
522 const DOMChanged = 1 << 1;
523 const PendingRestyles = 1 << 2;
524 const HighlightedDOMNodeChanged = 1 << 3;
525 const ThemeChanged = 1 << 4;
526 const ViewportChanged = 1 << 5;
527 const PaintWorkletLoaded = 1 << 6;
528 }
529}
530
531malloc_size_of_is_0!(RestyleReason);
532
533impl RestyleReason {
534 pub fn needs_restyle(&self) -> bool {
535 !self.is_empty()
536 }
537}
538
539#[derive(Debug, Default)]
541pub struct ReflowResult {
542 pub reflow_phases_run: ReflowPhasesRun,
544 pub reflow_statistics: ReflowStatistics,
545 pub pending_images: Vec<PendingImage>,
547 pub pending_rasterization_images: Vec<PendingRasterizationImage>,
549 pub pending_svg_elements_for_serialization: Vec<UntrustedNodeAddress>,
553 pub iframe_sizes: Option<IFrameSizes>,
559}
560
561bitflags! {
562 #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
564 pub struct ReflowPhasesRun: u8 {
565 const RanLayout = 1 << 0;
566 const CalculatedOverflow = 1 << 1;
567 const BuiltStackingContextTree = 1 << 2;
568 const BuiltDisplayList = 1 << 3;
569 const UpdatedScrollNodeOffset = 1 << 4;
570 const UpdatedImageData = 1 << 5;
574 }
575}
576
577impl ReflowPhasesRun {
578 pub fn needs_frame(&self) -> bool {
579 self.intersects(
580 Self::BuiltDisplayList | Self::UpdatedScrollNodeOffset | Self::UpdatedImageData,
581 )
582 }
583}
584
585#[derive(Debug, Default)]
586pub struct ReflowStatistics {
587 pub rebuilt_fragment_count: u32,
588 pub restyle_fragment_count: u32,
589}
590
591#[derive(Debug)]
594pub struct ReflowRequestRestyle {
595 pub reason: RestyleReason,
597 pub dirty_root: Option<TrustedNodeAddress>,
599 pub stylesheets_changed: bool,
601 pub pending_restyles: Vec<(TrustedNodeAddress, PendingRestyle)>,
603}
604
605#[derive(Debug)]
607pub struct ReflowRequest {
608 pub document: TrustedNodeAddress,
610 pub epoch: Epoch,
612 pub restyle: Option<ReflowRequestRestyle>,
614 pub viewport_details: ViewportDetails,
616 pub reflow_goal: ReflowGoal,
618 pub dom_count: u32,
620 pub origin: ImmutableOrigin,
622 pub animation_timeline_value: f64,
624 pub animations: DocumentAnimationSet,
626 pub animating_images: Arc<RwLock<AnimatingImages>>,
628 pub highlighted_dom_node: Option<OpaqueNode>,
630 pub document_context: WebFontDocumentContext,
632}
633
634impl ReflowRequest {
635 pub fn stylesheets_changed(&self) -> bool {
636 self.restyle
637 .as_ref()
638 .is_some_and(|restyle| restyle.stylesheets_changed)
639 }
640}
641
642#[derive(Debug, Default, MallocSizeOf)]
644pub struct PendingRestyle {
645 pub snapshot: Option<Snapshot>,
648
649 pub hint: RestyleHint,
651
652 pub damage: RestyleDamage,
654}
655
656#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, MallocSizeOf, PartialEq, Serialize)]
662pub enum FragmentType {
663 FragmentBody,
665 BeforePseudoContent,
667 AfterPseudoContent,
669}
670
671impl From<Option<PseudoElement>> for FragmentType {
672 fn from(value: Option<PseudoElement>) -> Self {
673 match value {
674 Some(PseudoElement::After) => FragmentType::AfterPseudoContent,
675 Some(PseudoElement::Before) => FragmentType::BeforePseudoContent,
676 _ => FragmentType::FragmentBody,
677 }
678 }
679}
680
681static NEXT_SPECIAL_SCROLL_ROOT_ID: AtomicU64 = AtomicU64::new(0);
685
686const SPECIAL_SCROLL_ROOT_ID_MASK: u64 = 0xffff;
689
690fn next_special_id() -> u64 {
692 ((NEXT_SPECIAL_SCROLL_ROOT_ID.fetch_add(1, Ordering::SeqCst) + 1) << 2) &
694 SPECIAL_SCROLL_ROOT_ID_MASK
695}
696
697pub fn combine_id_with_fragment_type(id: usize, fragment_type: FragmentType) -> u64 {
698 debug_assert_eq!(id & (fragment_type as usize), 0);
699 if fragment_type == FragmentType::FragmentBody {
700 id as u64
701 } else {
702 next_special_id() | (fragment_type as u64)
703 }
704}
705
706pub fn node_id_from_scroll_id(id: usize) -> Option<usize> {
707 if (id as u64 & !SPECIAL_SCROLL_ROOT_ID_MASK) != 0 {
708 return Some(id & !3);
709 }
710 None
711}
712
713#[derive(Clone, Debug, MallocSizeOf)]
714pub struct ImageAnimationState {
715 #[ignore_malloc_size_of = "RasterImage"]
716 pub image: Arc<RasterImage>,
717 pub active_frame: usize,
718 frame_start_time: f64,
719}
720
721impl ImageAnimationState {
722 pub fn new(image: Arc<RasterImage>, last_update_time: f64) -> Self {
723 Self {
724 image,
725 active_frame: 0,
726 frame_start_time: last_update_time,
727 }
728 }
729
730 pub fn image_key(&self) -> Option<ImageKey> {
731 self.image.id
732 }
733
734 pub fn duration_to_next_frame(&self, now: f64) -> Duration {
735 let frame_delay = self
736 .image
737 .frames
738 .get(self.active_frame)
739 .expect("Image frame should always be valid")
740 .delay
741 .unwrap_or_default();
742
743 let time_since_frame_start = (now - self.frame_start_time).max(0.0) * 1000.0;
744 let time_since_frame_start = Duration::from_secs_f64(time_since_frame_start);
745 frame_delay - time_since_frame_start.min(frame_delay)
746 }
747
748 pub fn update_frame_for_animation_timeline_value(&mut self, now: f64) -> bool {
752 if self.image.frames.len() <= 1 {
753 return false;
754 }
755 let image = &self.image;
756 let time_interval_since_last_update = now - self.frame_start_time;
757 let mut remain_time_interval = time_interval_since_last_update -
758 image
759 .frames
760 .get(self.active_frame)
761 .unwrap()
762 .delay()
763 .unwrap()
764 .as_secs_f64();
765 let mut next_active_frame_id = self.active_frame;
766 while remain_time_interval > 0.0 {
767 next_active_frame_id = (next_active_frame_id + 1) % image.frames.len();
768 remain_time_interval -= image
769 .frames
770 .get(next_active_frame_id)
771 .unwrap()
772 .delay()
773 .unwrap()
774 .as_secs_f64();
775 }
776 if self.active_frame == next_active_frame_id {
777 return false;
778 }
779 self.active_frame = next_active_frame_id;
780 self.frame_start_time = now;
781 true
782 }
783}
784
785#[derive(Debug)]
787pub struct ElementsFromPointResult {
788 pub node: OpaqueNode,
791 pub point_in_target: Point2D<f32, CSSPixel>,
794 pub cursor: Cursor,
797}
798
799bitflags! {
800 pub struct ElementsFromPointFlags: u8 {
801 const FindAll = 0b00000001;
804 }
805}
806
807#[derive(Debug, Default, MallocSizeOf)]
808pub struct AnimatingImages {
809 pub node_to_state_map: FxHashMap<OpaqueNode, ImageAnimationState>,
812 pub dirty: bool,
815}
816
817impl AnimatingImages {
818 pub fn maybe_insert_or_update(
819 &mut self,
820 node: OpaqueNode,
821 image: Arc<RasterImage>,
822 current_timeline_value: f64,
823 ) {
824 let entry = self.node_to_state_map.entry(node).or_insert_with(|| {
825 self.dirty = true;
826 ImageAnimationState::new(image.clone(), current_timeline_value)
827 });
828
829 if entry.image.id != image.id {
832 self.dirty = true;
833 *entry = ImageAnimationState::new(image.clone(), current_timeline_value);
834 }
835 }
836
837 pub fn remove(&mut self, node: OpaqueNode) {
838 if self.node_to_state_map.remove(&node).is_some() {
839 self.dirty = true;
840 }
841 }
842
843 pub fn clear_dirty(&mut self) -> bool {
845 std::mem::take(&mut self.dirty)
846 }
847
848 pub fn is_empty(&self) -> bool {
849 self.node_to_state_map.is_empty()
850 }
851}
852
853struct ThreadStateRestorer;
854
855impl ThreadStateRestorer {
856 fn new() -> Self {
857 #[cfg(debug_assertions)]
858 {
859 thread_state::exit(ThreadState::SCRIPT);
860 thread_state::enter(ThreadState::LAYOUT);
861 }
862 Self
863 }
864}
865
866impl Drop for ThreadStateRestorer {
867 fn drop(&mut self) {
868 #[cfg(debug_assertions)]
869 {
870 thread_state::exit(ThreadState::LAYOUT);
871 thread_state::enter(ThreadState::SCRIPT);
872 }
873 }
874}
875
876pub fn with_layout_state<R>(f: impl FnOnce() -> R) -> R {
882 let _guard = ThreadStateRestorer::new();
883 f()
884}
885
886#[cfg(test)]
887mod test {
888 use std::sync::Arc;
889 use std::time::Duration;
890
891 use pixels::{CorsStatus, ImageFrame, ImageMetadata, PixelFormat, RasterImage};
892
893 use crate::ImageAnimationState;
894
895 #[test]
896 fn test() {
897 let image_frames: Vec<ImageFrame> = std::iter::repeat_with(|| ImageFrame {
898 delay: Some(Duration::from_millis(100)),
899 byte_range: 0..1,
900 width: 100,
901 height: 100,
902 })
903 .take(10)
904 .collect();
905 let image = RasterImage {
906 metadata: ImageMetadata {
907 width: 100,
908 height: 100,
909 },
910 format: PixelFormat::BGRA8,
911 id: None,
912 bytes: Arc::new(vec![1]),
913 frames: image_frames,
914 cors_status: CorsStatus::Unsafe,
915 is_opaque: false,
916 };
917 let mut image_animation_state = ImageAnimationState::new(Arc::new(image), 0.0);
918
919 assert_eq!(image_animation_state.active_frame, 0);
920 assert_eq!(image_animation_state.frame_start_time, 0.0);
921 assert_eq!(
922 image_animation_state.update_frame_for_animation_timeline_value(0.101),
923 true
924 );
925 assert_eq!(image_animation_state.active_frame, 1);
926 assert_eq!(image_animation_state.frame_start_time, 0.101);
927 assert_eq!(
928 image_animation_state.update_frame_for_animation_timeline_value(0.116),
929 false
930 );
931 assert_eq!(image_animation_state.active_frame, 1);
932 assert_eq!(image_animation_state.frame_start_time, 0.101);
933 }
934}