1#![deny(unsafe_code)]
10
11mod layout_damage;
12pub mod wrapper_traits;
13
14use std::any::Any;
15use std::sync::Arc;
16use std::sync::atomic::{AtomicIsize, AtomicU64, Ordering};
17use std::thread::JoinHandle;
18use std::time::Duration;
19
20use app_units::Au;
21use atomic_refcell::AtomicRefCell;
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::ElementData;
50use style::dom::OpaqueNode;
51use style::invalidation::element::restyle_hints::RestyleHint;
52use style::media_queries::Device;
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::{Stylesheet, UrlExtraData};
58use style::values::computed::Overflow;
59use style_traits::CSSPixel;
60use webrender_api::units::{DeviceIntSize, LayoutPoint, LayoutVector2D};
61use webrender_api::{ExternalScrollId, ImageKey};
62
63pub trait GenericLayoutDataTrait: Any + MallocSizeOfTrait {
64 fn as_any(&self) -> &dyn Any;
65}
66
67pub type GenericLayoutData = dyn GenericLayoutDataTrait + Send + Sync;
68
69#[derive(MallocSizeOf)]
70pub struct StyleData {
71 pub element_data: AtomicRefCell<ElementData>,
76
77 pub parallel: DomParallelInfo,
79}
80
81impl Default for StyleData {
82 fn default() -> Self {
83 Self {
84 element_data: AtomicRefCell::new(ElementData::default()),
85 parallel: DomParallelInfo::default(),
86 }
87 }
88}
89
90#[derive(Default, MallocSizeOf)]
92pub struct DomParallelInfo {
93 pub children_to_process: AtomicIsize,
95}
96
97#[derive(Clone, Copy, Debug, Eq, PartialEq)]
98pub enum LayoutNodeType {
99 Element(LayoutElementType),
100 Text,
101}
102
103#[derive(Clone, Copy, Debug, Eq, PartialEq)]
104pub enum LayoutElementType {
105 Element,
106 HTMLBodyElement,
107 HTMLBRElement,
108 HTMLCanvasElement,
109 HTMLHtmlElement,
110 HTMLIFrameElement,
111 HTMLImageElement,
112 HTMLInputElement,
113 HTMLMediaElement,
114 HTMLObjectElement,
115 HTMLOptGroupElement,
116 HTMLOptionElement,
117 HTMLParagraphElement,
118 HTMLPreElement,
119 HTMLSelectElement,
120 HTMLTableCellElement,
121 HTMLTableColElement,
122 HTMLTableElement,
123 HTMLTableRowElement,
124 HTMLTableSectionElement,
125 HTMLTextAreaElement,
126 SVGImageElement,
127 SVGSVGElement,
128}
129
130pub struct HTMLCanvasData {
131 pub image_key: Option<ImageKey>,
132 pub width: u32,
133 pub height: u32,
134}
135
136pub struct SVGElementData<'dom> {
137 pub source: Option<Result<ServoUrl, ()>>,
139 pub width: Option<&'dom AttrValue>,
140 pub height: Option<&'dom AttrValue>,
141 pub svg_id: String,
142 pub view_box: Option<&'dom AttrValue>,
143}
144
145impl SVGElementData<'_> {
146 pub fn ratio_from_view_box(&self) -> Option<f32> {
147 let mut iter = self.view_box?.chars();
148 let _min_x = parse_integer(&mut iter).ok()?;
149 let _min_y = parse_integer(&mut iter).ok()?;
150
151 let width = parse_unsigned_integer(&mut iter).ok()?;
152 if width == 0 {
153 return None;
154 }
155
156 let height = parse_unsigned_integer(&mut iter).ok()?;
157 if height == 0 {
158 return None;
159 }
160
161 let mut iter = iter.skip_while(|c| char_is_whitespace(*c));
162 iter.next().is_none().then(|| width as f32 / height as f32)
163 }
164}
165
166#[derive(Clone, Copy, Debug, Eq, PartialEq)]
168pub struct TrustedNodeAddress(pub *const c_void);
169
170#[expect(unsafe_code)]
171unsafe impl Send for TrustedNodeAddress {}
172
173#[derive(Debug)]
175pub enum PendingImageState {
176 Unrequested(ServoUrl),
177 PendingResponse,
178}
179
180#[derive(Debug, MallocSizeOf)]
182pub enum LayoutImageDestination {
183 BoxTreeConstruction,
184 DisplayListBuilding,
185}
186
187#[derive(Debug)]
191pub struct PendingImage {
192 pub state: PendingImageState,
193 pub node: UntrustedNodeAddress,
194 pub id: PendingImageId,
195 pub origin: ImmutableOrigin,
196 pub destination: LayoutImageDestination,
197}
198
199#[derive(Debug)]
203pub struct PendingRasterizationImage {
204 pub node: UntrustedNodeAddress,
205 pub id: PendingImageId,
206 pub size: DeviceIntSize,
207}
208
209#[derive(Clone, Copy, Debug, MallocSizeOf)]
210pub struct MediaFrame {
211 pub image_key: webrender_api::ImageKey,
212 pub width: i32,
213 pub height: i32,
214}
215
216pub struct MediaMetadata {
217 pub width: u32,
218 pub height: u32,
219}
220
221pub struct HTMLMediaData {
222 pub current_frame: Option<MediaFrame>,
223 pub metadata: Option<MediaMetadata>,
224}
225
226pub struct LayoutConfig {
227 pub id: PipelineId,
228 pub webview_id: WebViewId,
229 pub url: ServoUrl,
230 pub is_iframe: bool,
231 pub script_chan: GenericSender<ScriptThreadMessage>,
232 pub image_cache: Arc<dyn ImageCache>,
233 pub font_context: Arc<FontContext>,
234 pub time_profiler_chan: time::ProfilerChan,
235 pub paint_api: CrossProcessPaintApi,
236 pub viewport_details: ViewportDetails,
237 pub theme: Theme,
238}
239
240pub struct PropertyRegistration {
241 pub name: String,
242 pub syntax: String,
243 pub initial_value: Option<String>,
244 pub inherits: bool,
245 pub url_data: UrlExtraData,
246}
247
248#[derive(Debug)]
249pub enum RegisterPropertyError {
250 InvalidName,
251 AlreadyRegistered,
252 InvalidSyntax,
253 InvalidInitialValue,
254 InitialValueNotComputationallyIndependent,
255 NoInitialValue,
256}
257
258pub trait LayoutFactory: Send + Sync {
259 fn create(&self, config: LayoutConfig) -> Box<dyn Layout>;
260}
261
262pub trait Layout {
263 fn device(&self) -> &Device;
266
267 fn set_theme(&mut self, theme: Theme) -> bool;
271
272 fn set_viewport_details(&mut self, viewport_details: ViewportDetails) -> bool;
276
277 fn load_web_fonts_from_stylesheet(
280 &self,
281 stylesheet: &ServoArc<Stylesheet>,
282 font_context: &WebFontDocumentContext,
283 );
284
285 fn add_stylesheet(
289 &mut self,
290 stylesheet: ServoArc<Stylesheet>,
291 before_stylsheet: Option<ServoArc<Stylesheet>>,
292 font_context: &WebFontDocumentContext,
293 );
294
295 fn exit_now(&mut self);
297
298 fn collect_reports(&self, reports: &mut Vec<Report>, ops: &mut MallocSizeOfOps);
301
302 fn set_quirks_mode(&mut self, quirks_mode: QuirksMode);
304
305 fn remove_stylesheet(&mut self, stylesheet: ServoArc<Stylesheet>);
307
308 fn remove_cached_image(&mut self, image_url: &ServoUrl);
310
311 fn reflow(&mut self, reflow_request: ReflowRequest) -> Option<ReflowResult>;
313
314 fn ensure_stacking_context_tree(&self, viewport_details: ViewportDetails);
317
318 fn register_paint_worklet_modules(
320 &mut self,
321 name: Atom,
322 properties: Vec<Atom>,
323 painter: Box<dyn Painter>,
324 );
325
326 fn set_scroll_offsets_from_renderer(
328 &mut self,
329 scroll_states: &FxHashMap<ExternalScrollId, LayoutVector2D>,
330 );
331
332 fn scroll_offset(&self, id: ExternalScrollId) -> Option<LayoutVector2D>;
335
336 fn needs_new_display_list(&self) -> bool;
338
339 fn set_needs_new_display_list(&self);
341
342 fn query_padding(&self, node: TrustedNodeAddress) -> Option<PhysicalSides>;
343 fn query_box_area(
344 &self,
345 node: TrustedNodeAddress,
346 area: BoxAreaType,
347 exclude_transform_and_inline: bool,
348 ) -> Option<Rect<Au, CSSPixel>>;
349 fn query_box_areas(
350 &self,
351 node: TrustedNodeAddress,
352 area: BoxAreaType,
353 ) -> Vec<Rect<Au, CSSPixel>>;
354 fn query_client_rect(&self, node: TrustedNodeAddress) -> Rect<i32, CSSPixel>;
355 fn query_current_css_zoom(&self, node: TrustedNodeAddress) -> f32;
356 fn query_element_inner_outer_text(&self, node: TrustedNodeAddress) -> String;
357 fn query_offset_parent(&self, node: TrustedNodeAddress) -> OffsetParentResponse;
358 fn query_scroll_container(
361 &self,
362 node: Option<TrustedNodeAddress>,
363 flags: ScrollContainerQueryFlags,
364 ) -> Option<ScrollContainerResponse>;
365 fn query_resolved_style(
366 &self,
367 node: TrustedNodeAddress,
368 pseudo: Option<PseudoElement>,
369 property_id: PropertyId,
370 animations: DocumentAnimationSet,
371 animation_timeline_value: f64,
372 ) -> String;
373 fn query_resolved_font_style(
374 &self,
375 node: TrustedNodeAddress,
376 value: &str,
377 animations: DocumentAnimationSet,
378 animation_timeline_value: f64,
379 ) -> Option<ServoArc<Font>>;
380 fn query_scrolling_area(&self, node: Option<TrustedNodeAddress>) -> Rect<i32, CSSPixel>;
381 fn query_text_index(
383 &self,
384 node: TrustedNodeAddress,
385 point: Point2D<Au, CSSPixel>,
386 ) -> Option<usize>;
387 fn query_elements_from_point(
388 &self,
389 point: LayoutPoint,
390 flags: ElementsFromPointFlags,
391 ) -> Vec<ElementsFromPointResult>;
392 fn register_custom_property(
393 &mut self,
394 property_registration: PropertyRegistration,
395 ) -> Result<(), RegisterPropertyError>;
396}
397
398pub trait ScriptThreadFactory {
402 fn create(
404 state: InitialScriptState,
405 layout_factory: Arc<dyn LayoutFactory>,
406 image_cache_factory: Arc<dyn ImageCacheFactory>,
407 background_hang_monitor_register: Box<dyn BackgroundHangMonitorRegister>,
408 ) -> JoinHandle<()>;
409}
410
411#[derive(Copy, Clone)]
414pub enum BoxAreaType {
415 Content,
416 Padding,
417 Border,
418}
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}