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 atomic_refcell::AtomicRefCell;
23use background_hang_monitor_api::BackgroundHangMonitorRegister;
24use base::Epoch;
25use base::generic_channel::GenericSender;
26use base::id::{BrowsingContextId, PipelineId, WebViewId};
27use bitflags::bitflags;
28use embedder_traits::{Cursor, Theme, UntrustedNodeAddress, ViewportDetails};
29use euclid::{Point2D, Rect};
30use fonts::{FontContext, WebFontDocumentContext};
31pub use layout_damage::LayoutDamage;
32use libc::c_void;
33use malloc_size_of::{MallocSizeOf as MallocSizeOfTrait, MallocSizeOfOps, malloc_size_of_is_0};
34use malloc_size_of_derive::MallocSizeOf;
35use net_traits::image_cache::{ImageCache, ImageCacheFactory, PendingImageId};
36use paint_api::CrossProcessPaintApi;
37use parking_lot::RwLock;
38use pixels::RasterImage;
39use profile_traits::mem::Report;
40use profile_traits::time;
41use rustc_hash::FxHashMap;
42use script_traits::{InitialScriptState, Painter, ScriptThreadMessage};
43use serde::{Deserialize, Serialize};
44use servo_arc::Arc as ServoArc;
45use servo_url::{ImmutableOrigin, ServoUrl};
46use style::Atom;
47use style::animation::DocumentAnimationSet;
48use style::attr::{AttrValue, parse_integer, parse_unsigned_integer};
49use style::context::QuirksMode;
50use style::data::ElementData;
51use style::dom::OpaqueNode;
52use style::invalidation::element::restyle_hints::RestyleHint;
53use style::media_queries::Device;
54use style::properties::style_structs::Font;
55use style::properties::{ComputedValues, PropertyId};
56use style::selector_parser::{PseudoElement, RestyleDamage, Snapshot};
57use style::str::char_is_whitespace;
58use style::stylesheets::{DocumentStyleSheet, Stylesheet, UrlExtraData};
59use style::values::computed::Overflow;
60use style_traits::CSSPixel;
61use webrender_api::units::{DeviceIntSize, LayoutPoint, LayoutVector2D};
62use webrender_api::{ExternalScrollId, ImageKey};
63
64pub trait GenericLayoutDataTrait: Any + MallocSizeOfTrait {
65 fn as_any(&self) -> &dyn Any;
66}
67
68pub type GenericLayoutData = dyn GenericLayoutDataTrait + Send + Sync;
69
70#[derive(MallocSizeOf)]
71pub struct StyleData {
72 pub element_data: AtomicRefCell<ElementData>,
77
78 pub parallel: DomParallelInfo,
80}
81
82impl Default for StyleData {
83 fn default() -> Self {
84 Self {
85 element_data: AtomicRefCell::new(ElementData::default()),
86 parallel: DomParallelInfo::default(),
87 }
88 }
89}
90
91#[derive(Default, MallocSizeOf)]
93pub struct DomParallelInfo {
94 pub children_to_process: AtomicIsize,
96}
97
98#[derive(Clone, Copy, Debug, Eq, PartialEq)]
99pub enum LayoutNodeType {
100 Element(LayoutElementType),
101 Text,
102}
103
104#[derive(Clone, Copy, Debug, Eq, PartialEq)]
105pub enum LayoutElementType {
106 Element,
107 HTMLBodyElement,
108 HTMLBRElement,
109 HTMLCanvasElement,
110 HTMLHtmlElement,
111 HTMLIFrameElement,
112 HTMLImageElement,
113 HTMLInputElement,
114 HTMLMediaElement,
115 HTMLObjectElement,
116 HTMLOptGroupElement,
117 HTMLOptionElement,
118 HTMLParagraphElement,
119 HTMLPreElement,
120 HTMLSelectElement,
121 HTMLTableCellElement,
122 HTMLTableColElement,
123 HTMLTableElement,
124 HTMLTableRowElement,
125 HTMLTableSectionElement,
126 HTMLTextAreaElement,
127 SVGImageElement,
128 SVGSVGElement,
129}
130
131pub struct HTMLCanvasData {
132 pub image_key: Option<ImageKey>,
133 pub width: u32,
134 pub height: u32,
135}
136
137pub struct SVGElementData<'dom> {
138 pub source: Option<Result<ServoUrl, ()>>,
140 pub width: Option<&'dom AttrValue>,
141 pub height: Option<&'dom AttrValue>,
142 pub svg_id: String,
143 pub view_box: Option<&'dom AttrValue>,
144}
145
146impl SVGElementData<'_> {
147 pub fn ratio_from_view_box(&self) -> Option<f32> {
148 let mut iter = self.view_box?.chars();
149 let _min_x = parse_integer(&mut iter).ok()?;
150 let _min_y = parse_integer(&mut iter).ok()?;
151
152 let width = parse_unsigned_integer(&mut iter).ok()?;
153 if width == 0 {
154 return None;
155 }
156
157 let height = parse_unsigned_integer(&mut iter).ok()?;
158 if height == 0 {
159 return None;
160 }
161
162 let mut iter = iter.skip_while(|c| char_is_whitespace(*c));
163 iter.next().is_none().then(|| width as f32 / height as f32)
164 }
165}
166
167#[derive(Clone, Copy, Debug, Eq, PartialEq)]
169pub struct TrustedNodeAddress(pub *const c_void);
170
171#[expect(unsafe_code)]
172unsafe impl Send for TrustedNodeAddress {}
173
174#[derive(Debug)]
176pub enum PendingImageState {
177 Unrequested(ServoUrl),
178 PendingResponse,
179}
180
181#[derive(Debug, MallocSizeOf)]
183pub enum LayoutImageDestination {
184 BoxTreeConstruction,
185 DisplayListBuilding,
186}
187
188#[derive(Debug)]
192pub struct PendingImage {
193 pub state: PendingImageState,
194 pub node: UntrustedNodeAddress,
195 pub id: PendingImageId,
196 pub origin: ImmutableOrigin,
197 pub destination: LayoutImageDestination,
198}
199
200#[derive(Debug)]
204pub struct PendingRasterizationImage {
205 pub node: UntrustedNodeAddress,
206 pub id: PendingImageId,
207 pub size: DeviceIntSize,
208}
209
210#[derive(Clone, Copy, Debug, MallocSizeOf)]
211pub struct MediaFrame {
212 pub image_key: webrender_api::ImageKey,
213 pub width: i32,
214 pub height: i32,
215}
216
217pub struct MediaMetadata {
218 pub width: u32,
219 pub height: u32,
220}
221
222pub struct HTMLMediaData {
223 pub current_frame: Option<MediaFrame>,
224 pub metadata: Option<MediaMetadata>,
225}
226
227pub struct LayoutConfig {
228 pub id: PipelineId,
229 pub webview_id: WebViewId,
230 pub url: ServoUrl,
231 pub is_iframe: bool,
232 pub script_chan: GenericSender<ScriptThreadMessage>,
233 pub image_cache: Arc<dyn ImageCache>,
234 pub font_context: Arc<FontContext>,
235 pub time_profiler_chan: time::ProfilerChan,
236 pub paint_api: CrossProcessPaintApi,
237 pub viewport_details: ViewportDetails,
238 pub user_stylesheets: Rc<Vec<DocumentStyleSheet>>,
239 pub theme: Theme,
240}
241
242pub struct PropertyRegistration {
243 pub name: String,
244 pub syntax: String,
245 pub initial_value: Option<String>,
246 pub inherits: bool,
247 pub url_data: UrlExtraData,
248}
249
250#[derive(Debug)]
251pub enum RegisterPropertyError {
252 InvalidName,
253 AlreadyRegistered,
254 InvalidSyntax,
255 InvalidInitialValue,
256 InitialValueNotComputationallyIndependent,
257 NoInitialValue,
258}
259
260pub trait LayoutFactory: Send + Sync {
261 fn create(&self, config: LayoutConfig) -> Box<dyn Layout>;
262}
263
264pub trait Layout {
265 fn device(&self) -> &Device;
268
269 fn set_theme(&mut self, theme: Theme) -> bool;
273
274 fn set_viewport_details(&mut self, viewport_details: ViewportDetails) -> bool;
278
279 fn load_web_fonts_from_stylesheet(
282 &self,
283 stylesheet: &ServoArc<Stylesheet>,
284 font_context: &WebFontDocumentContext,
285 );
286
287 fn add_stylesheet(
291 &mut self,
292 stylesheet: ServoArc<Stylesheet>,
293 before_stylsheet: Option<ServoArc<Stylesheet>>,
294 font_context: &WebFontDocumentContext,
295 );
296
297 fn exit_now(&mut self);
299
300 fn collect_reports(&self, reports: &mut Vec<Report>, ops: &mut MallocSizeOfOps);
303
304 fn set_quirks_mode(&mut self, quirks_mode: QuirksMode);
306
307 fn remove_stylesheet(&mut self, stylesheet: ServoArc<Stylesheet>);
309
310 fn remove_cached_image(&mut self, image_url: &ServoUrl);
312
313 fn reflow(&mut self, reflow_request: ReflowRequest) -> Option<ReflowResult>;
315
316 fn ensure_stacking_context_tree(&self, viewport_details: ViewportDetails);
319
320 fn register_paint_worklet_modules(
322 &mut self,
323 name: Atom,
324 properties: Vec<Atom>,
325 painter: Box<dyn Painter>,
326 );
327
328 fn set_scroll_offsets_from_renderer(
330 &mut self,
331 scroll_states: &FxHashMap<ExternalScrollId, LayoutVector2D>,
332 );
333
334 fn scroll_offset(&self, id: ExternalScrollId) -> Option<LayoutVector2D>;
337
338 fn needs_new_display_list(&self) -> bool;
340
341 fn set_needs_new_display_list(&self);
343
344 fn query_padding(&self, node: TrustedNodeAddress) -> Option<PhysicalSides>;
345 fn query_box_area(
346 &self,
347 node: TrustedNodeAddress,
348 area: BoxAreaType,
349 exclude_transform_and_inline: bool,
350 ) -> Option<Rect<Au, CSSPixel>>;
351 fn query_box_areas(&self, node: TrustedNodeAddress, area: BoxAreaType) -> CSSPixelRectIterator;
352 fn query_client_rect(&self, node: TrustedNodeAddress) -> Rect<i32, CSSPixel>;
353 fn query_current_css_zoom(&self, node: TrustedNodeAddress) -> f32;
354 fn query_element_inner_outer_text(&self, node: TrustedNodeAddress) -> String;
355 fn query_offset_parent(&self, node: TrustedNodeAddress) -> OffsetParentResponse;
356 fn query_scroll_container(
359 &self,
360 node: Option<TrustedNodeAddress>,
361 flags: ScrollContainerQueryFlags,
362 ) -> Option<ScrollContainerResponse>;
363 fn query_resolved_style(
364 &self,
365 node: TrustedNodeAddress,
366 pseudo: Option<PseudoElement>,
367 property_id: PropertyId,
368 animations: DocumentAnimationSet,
369 animation_timeline_value: f64,
370 ) -> String;
371 fn query_resolved_font_style(
372 &self,
373 node: TrustedNodeAddress,
374 value: &str,
375 animations: DocumentAnimationSet,
376 animation_timeline_value: f64,
377 ) -> Option<ServoArc<Font>>;
378 fn query_scrolling_area(&self, node: Option<TrustedNodeAddress>) -> Rect<i32, CSSPixel>;
379 fn query_text_index(
381 &self,
382 node: TrustedNodeAddress,
383 point: Point2D<Au, CSSPixel>,
384 ) -> Option<usize>;
385 fn query_elements_from_point(
386 &self,
387 point: LayoutPoint,
388 flags: ElementsFromPointFlags,
389 ) -> Vec<ElementsFromPointResult>;
390 fn register_custom_property(
391 &mut self,
392 property_registration: PropertyRegistration,
393 ) -> Result<(), RegisterPropertyError>;
394}
395
396pub trait ScriptThreadFactory {
400 fn create(
402 state: InitialScriptState,
403 layout_factory: Arc<dyn LayoutFactory>,
404 image_cache_factory: Arc<dyn ImageCacheFactory>,
405 background_hang_monitor_register: Box<dyn BackgroundHangMonitorRegister>,
406 ) -> JoinHandle<()>;
407}
408
409#[derive(Copy, Clone)]
412pub enum BoxAreaType {
413 Content,
414 Padding,
415 Border,
416}
417
418pub type CSSPixelRectIterator = Box<dyn Iterator<Item = Rect<Au, CSSPixel>>>;
419
420#[derive(Default)]
421pub struct PhysicalSides {
422 pub left: Au,
423 pub top: Au,
424 pub right: Au,
425 pub bottom: Au,
426}
427
428#[derive(Clone, Default)]
429pub struct OffsetParentResponse {
430 pub node_address: Option<UntrustedNodeAddress>,
431 pub rect: Rect<Au, CSSPixel>,
432}
433
434bitflags! {
435 #[derive(PartialEq)]
436 pub struct ScrollContainerQueryFlags: u8 {
437 const ForScrollParent = 1 << 0;
439 const Inclusive = 1 << 1;
441 }
442}
443
444#[derive(Clone, Copy, Debug, MallocSizeOf)]
445pub struct AxesOverflow {
446 pub x: Overflow,
447 pub y: Overflow,
448}
449
450impl Default for AxesOverflow {
451 fn default() -> Self {
452 Self {
453 x: Overflow::Visible,
454 y: Overflow::Visible,
455 }
456 }
457}
458
459impl From<&ComputedValues> for AxesOverflow {
460 fn from(style: &ComputedValues) -> Self {
461 Self {
462 x: style.clone_overflow_x(),
463 y: style.clone_overflow_y(),
464 }
465 }
466}
467
468impl AxesOverflow {
469 pub fn to_scrollable(&self) -> Self {
470 Self {
471 x: self.x.to_scrollable(),
472 y: self.y.to_scrollable(),
473 }
474 }
475}
476
477#[derive(Clone)]
478pub enum ScrollContainerResponse {
479 Viewport(AxesOverflow),
480 Element(UntrustedNodeAddress, AxesOverflow),
481}
482
483#[derive(Debug, PartialEq)]
484pub enum QueryMsg {
485 BoxArea,
486 BoxAreas,
487 ClientRectQuery,
488 CurrentCSSZoomQuery,
489 ElementInnerOuterTextQuery,
490 ElementsFromPoint,
491 InnerWindowDimensionsQuery,
492 NodesFromPointQuery,
493 OffsetParentQuery,
494 ScrollParentQuery,
495 ResolvedFontStyleQuery,
496 ResolvedStyleQuery,
497 ScrollingAreaOrOffsetQuery,
498 StyleQuery,
499 TextIndexQuery,
500 PaddingQuery,
501}
502
503#[derive(Debug, PartialEq)]
509pub enum ReflowGoal {
510 UpdateTheRendering,
513
514 LayoutQuery(QueryMsg),
517
518 UpdateScrollNode(ExternalScrollId, LayoutVector2D),
522}
523
524#[derive(Clone, Debug, MallocSizeOf)]
525pub struct IFrameSize {
526 pub browsing_context_id: BrowsingContextId,
527 pub pipeline_id: PipelineId,
528 pub viewport_details: ViewportDetails,
529}
530
531pub type IFrameSizes = FxHashMap<BrowsingContextId, IFrameSize>;
532
533bitflags! {
534 #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
537 pub struct RestyleReason: u16 {
538 const StylesheetsChanged = 1 << 0;
539 const DOMChanged = 1 << 1;
540 const PendingRestyles = 1 << 2;
541 const HighlightedDOMNodeChanged = 1 << 3;
542 const ThemeChanged = 1 << 4;
543 const ViewportChanged = 1 << 5;
544 const PaintWorkletLoaded = 1 << 6;
545 }
546}
547
548malloc_size_of_is_0!(RestyleReason);
549
550impl RestyleReason {
551 pub fn needs_restyle(&self) -> bool {
552 !self.is_empty()
553 }
554}
555
556#[derive(Debug, Default)]
558pub struct ReflowResult {
559 pub reflow_phases_run: ReflowPhasesRun,
561 pub pending_images: Vec<PendingImage>,
563 pub pending_rasterization_images: Vec<PendingRasterizationImage>,
565 pub pending_svg_elements_for_serialization: Vec<UntrustedNodeAddress>,
569 pub iframe_sizes: Option<IFrameSizes>,
575}
576
577bitflags! {
578 #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
580 pub struct ReflowPhasesRun: u8 {
581 const RanLayout = 1 << 0;
582 const CalculatedOverflow = 1 << 1;
583 const BuiltStackingContextTree = 1 << 2;
584 const BuiltDisplayList = 1 << 3;
585 const UpdatedScrollNodeOffset = 1 << 4;
586 const UpdatedImageData = 1 << 5;
590 }
591}
592
593impl ReflowPhasesRun {
594 pub fn needs_frame(&self) -> bool {
595 self.intersects(
596 Self::BuiltDisplayList | Self::UpdatedScrollNodeOffset | Self::UpdatedImageData,
597 )
598 }
599}
600
601#[derive(Debug)]
604pub struct ReflowRequestRestyle {
605 pub reason: RestyleReason,
607 pub dirty_root: Option<TrustedNodeAddress>,
609 pub stylesheets_changed: bool,
611 pub pending_restyles: Vec<(TrustedNodeAddress, PendingRestyle)>,
613}
614
615#[derive(Debug)]
617pub struct ReflowRequest {
618 pub document: TrustedNodeAddress,
620 pub epoch: Epoch,
622 pub restyle: Option<ReflowRequestRestyle>,
624 pub viewport_details: ViewportDetails,
626 pub reflow_goal: ReflowGoal,
628 pub dom_count: u32,
630 pub origin: ImmutableOrigin,
632 pub animation_timeline_value: f64,
634 pub animations: DocumentAnimationSet,
636 pub animating_images: Arc<RwLock<AnimatingImages>>,
638 pub highlighted_dom_node: Option<OpaqueNode>,
640 pub document_context: WebFontDocumentContext,
642}
643
644impl ReflowRequest {
645 pub fn stylesheets_changed(&self) -> bool {
646 self.restyle
647 .as_ref()
648 .is_some_and(|restyle| restyle.stylesheets_changed)
649 }
650}
651
652#[derive(Debug, Default, MallocSizeOf)]
654pub struct PendingRestyle {
655 pub snapshot: Option<Snapshot>,
658
659 pub hint: RestyleHint,
661
662 pub damage: RestyleDamage,
664}
665
666#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, MallocSizeOf, PartialEq, Serialize)]
672pub enum FragmentType {
673 FragmentBody,
675 BeforePseudoContent,
677 AfterPseudoContent,
679}
680
681impl From<Option<PseudoElement>> for FragmentType {
682 fn from(value: Option<PseudoElement>) -> Self {
683 match value {
684 Some(PseudoElement::After) => FragmentType::AfterPseudoContent,
685 Some(PseudoElement::Before) => FragmentType::BeforePseudoContent,
686 _ => FragmentType::FragmentBody,
687 }
688 }
689}
690
691static NEXT_SPECIAL_SCROLL_ROOT_ID: AtomicU64 = AtomicU64::new(0);
695
696const SPECIAL_SCROLL_ROOT_ID_MASK: u64 = 0xffff;
699
700fn next_special_id() -> u64 {
702 ((NEXT_SPECIAL_SCROLL_ROOT_ID.fetch_add(1, Ordering::SeqCst) + 1) << 2) &
704 SPECIAL_SCROLL_ROOT_ID_MASK
705}
706
707pub fn combine_id_with_fragment_type(id: usize, fragment_type: FragmentType) -> u64 {
708 debug_assert_eq!(id & (fragment_type as usize), 0);
709 if fragment_type == FragmentType::FragmentBody {
710 id as u64
711 } else {
712 next_special_id() | (fragment_type as u64)
713 }
714}
715
716pub fn node_id_from_scroll_id(id: usize) -> Option<usize> {
717 if (id as u64 & !SPECIAL_SCROLL_ROOT_ID_MASK) != 0 {
718 return Some(id & !3);
719 }
720 None
721}
722
723#[derive(Clone, Debug, MallocSizeOf)]
724pub struct ImageAnimationState {
725 #[ignore_malloc_size_of = "RasterImage"]
726 pub image: Arc<RasterImage>,
727 pub active_frame: usize,
728 frame_start_time: f64,
729}
730
731impl ImageAnimationState {
732 pub fn new(image: Arc<RasterImage>, last_update_time: f64) -> Self {
733 Self {
734 image,
735 active_frame: 0,
736 frame_start_time: last_update_time,
737 }
738 }
739
740 pub fn image_key(&self) -> Option<ImageKey> {
741 self.image.id
742 }
743
744 pub fn duration_to_next_frame(&self, now: f64) -> Duration {
745 let frame_delay = self
746 .image
747 .frames
748 .get(self.active_frame)
749 .expect("Image frame should always be valid")
750 .delay
751 .unwrap_or_default();
752
753 let time_since_frame_start = (now - self.frame_start_time).max(0.0) * 1000.0;
754 let time_since_frame_start = Duration::from_secs_f64(time_since_frame_start);
755 frame_delay - time_since_frame_start.min(frame_delay)
756 }
757
758 pub fn update_frame_for_animation_timeline_value(&mut self, now: f64) -> bool {
762 if self.image.frames.len() <= 1 {
763 return false;
764 }
765 let image = &self.image;
766 let time_interval_since_last_update = now - self.frame_start_time;
767 let mut remain_time_interval = time_interval_since_last_update -
768 image
769 .frames
770 .get(self.active_frame)
771 .unwrap()
772 .delay()
773 .unwrap()
774 .as_secs_f64();
775 let mut next_active_frame_id = self.active_frame;
776 while remain_time_interval > 0.0 {
777 next_active_frame_id = (next_active_frame_id + 1) % image.frames.len();
778 remain_time_interval -= image
779 .frames
780 .get(next_active_frame_id)
781 .unwrap()
782 .delay()
783 .unwrap()
784 .as_secs_f64();
785 }
786 if self.active_frame == next_active_frame_id {
787 return false;
788 }
789 self.active_frame = next_active_frame_id;
790 self.frame_start_time = now;
791 true
792 }
793}
794
795#[derive(Debug)]
797pub struct ElementsFromPointResult {
798 pub node: OpaqueNode,
801 pub point_in_target: Point2D<f32, CSSPixel>,
804 pub cursor: Cursor,
807}
808
809bitflags! {
810 pub struct ElementsFromPointFlags: u8 {
811 const FindAll = 0b00000001;
814 }
815}
816
817#[derive(Debug, Default, MallocSizeOf)]
818pub struct AnimatingImages {
819 pub node_to_state_map: FxHashMap<OpaqueNode, ImageAnimationState>,
822 pub dirty: bool,
825}
826
827impl AnimatingImages {
828 pub fn maybe_insert_or_update(
829 &mut self,
830 node: OpaqueNode,
831 image: Arc<RasterImage>,
832 current_timeline_value: f64,
833 ) {
834 let entry = self.node_to_state_map.entry(node).or_insert_with(|| {
835 self.dirty = true;
836 ImageAnimationState::new(image.clone(), current_timeline_value)
837 });
838
839 if entry.image.id != image.id {
842 self.dirty = true;
843 *entry = ImageAnimationState::new(image.clone(), current_timeline_value);
844 }
845 }
846
847 pub fn remove(&mut self, node: OpaqueNode) {
848 if self.node_to_state_map.remove(&node).is_some() {
849 self.dirty = true;
850 }
851 }
852
853 pub fn clear_dirty(&mut self) -> bool {
855 std::mem::take(&mut self.dirty)
856 }
857
858 pub fn is_empty(&self) -> bool {
859 self.node_to_state_map.is_empty()
860 }
861}
862
863#[cfg(test)]
864mod test {
865 use std::sync::Arc;
866 use std::time::Duration;
867
868 use pixels::{CorsStatus, ImageFrame, ImageMetadata, PixelFormat, RasterImage};
869
870 use crate::ImageAnimationState;
871
872 #[test]
873 fn test() {
874 let image_frames: Vec<ImageFrame> = std::iter::repeat_with(|| ImageFrame {
875 delay: Some(Duration::from_millis(100)),
876 byte_range: 0..1,
877 width: 100,
878 height: 100,
879 })
880 .take(10)
881 .collect();
882 let image = RasterImage {
883 metadata: ImageMetadata {
884 width: 100,
885 height: 100,
886 },
887 format: PixelFormat::BGRA8,
888 id: None,
889 bytes: Arc::new(vec![1]),
890 frames: image_frames,
891 cors_status: CorsStatus::Unsafe,
892 is_opaque: false,
893 };
894 let mut image_animation_state = ImageAnimationState::new(Arc::new(image), 0.0);
895
896 assert_eq!(image_animation_state.active_frame, 0);
897 assert_eq!(image_animation_state.frame_start_time, 0.0);
898 assert_eq!(
899 image_animation_state.update_frame_for_animation_timeline_value(0.101),
900 true
901 );
902 assert_eq!(image_animation_state.active_frame, 1);
903 assert_eq!(image_animation_state.frame_start_time, 0.101);
904 assert_eq!(
905 image_animation_state.update_frame_for_animation_timeline_value(0.116),
906 false
907 );
908 assert_eq!(image_animation_state.active_frame, 1);
909 assert_eq!(image_animation_state.frame_start_time, 0.101);
910 }
911}