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;
18use std::thread::JoinHandle;
19use std::time::Duration;
20
21use app_units::Au;
22use background_hang_monitor_api::BackgroundHangMonitorRegister;
23use bitflags::bitflags;
24use embedder_traits::{Cursor, Theme, UntrustedNodeAddress, ViewportDetails};
25use euclid::{Point2D, Rect};
26use fonts::{FontContext, WebFontDocumentContext};
27pub use layout_damage::LayoutDamage;
28use libc::c_void;
29use malloc_size_of::{MallocSizeOf as MallocSizeOfTrait, MallocSizeOfOps, malloc_size_of_is_0};
30use malloc_size_of_derive::MallocSizeOf;
31use net_traits::image_cache::{ImageCache, ImageCacheFactory, PendingImageId};
32use paint_api::CrossProcessPaintApi;
33use parking_lot::RwLock;
34use pixels::RasterImage;
35use profile_traits::mem::Report;
36use profile_traits::time;
37use rustc_hash::FxHashMap;
38use script_traits::{InitialScriptState, Painter, ScriptThreadMessage};
39use serde::{Deserialize, Serialize};
40use servo_arc::Arc as ServoArc;
41use servo_base::Epoch;
42use servo_base::generic_channel::GenericSender;
43use servo_base::id::{BrowsingContextId, PipelineId, WebViewId};
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}
233
234pub trait LayoutFactory: Send + Sync {
235 fn create(&self, config: LayoutConfig) -> Box<dyn Layout>;
236}
237
238pub trait Layout {
239 fn device(&self) -> &Device;
242
243 fn set_theme(&mut self, theme: Theme) -> bool;
247
248 fn set_viewport_details(&mut self, viewport_details: ViewportDetails) -> bool;
252
253 fn load_web_fonts_from_stylesheet(
256 &self,
257 stylesheet: &ServoArc<Stylesheet>,
258 font_context: &WebFontDocumentContext,
259 );
260
261 fn add_stylesheet(
265 &mut self,
266 stylesheet: ServoArc<Stylesheet>,
267 before_stylsheet: Option<ServoArc<Stylesheet>>,
268 font_context: &WebFontDocumentContext,
269 );
270
271 fn exit_now(&mut self);
273
274 fn collect_reports(&self, reports: &mut Vec<Report>, ops: &mut MallocSizeOfOps);
277
278 fn set_quirks_mode(&mut self, quirks_mode: QuirksMode);
280
281 fn remove_stylesheet(&mut self, stylesheet: ServoArc<Stylesheet>);
283
284 fn remove_cached_image(&mut self, image_url: &ServoUrl);
286
287 fn reflow(&mut self, reflow_request: ReflowRequest) -> Option<ReflowResult>;
289
290 fn ensure_stacking_context_tree(&self, viewport_details: ViewportDetails);
293
294 fn register_paint_worklet_modules(
296 &mut self,
297 name: Atom,
298 properties: Vec<Atom>,
299 painter: Box<dyn Painter>,
300 );
301
302 fn set_scroll_offsets_from_renderer(
304 &mut self,
305 scroll_states: &FxHashMap<ExternalScrollId, LayoutVector2D>,
306 );
307
308 fn scroll_offset(&self, id: ExternalScrollId) -> Option<LayoutVector2D>;
311
312 fn needs_new_display_list(&self) -> bool;
314
315 fn set_needs_new_display_list(&self);
317
318 fn query_padding(&self, node: TrustedNodeAddress) -> Option<PhysicalSides>;
319 fn query_box_area(
320 &self,
321 node: TrustedNodeAddress,
322 area: BoxAreaType,
323 exclude_transform_and_inline: bool,
324 ) -> Option<Rect<Au, CSSPixel>>;
325 fn query_box_areas(&self, node: TrustedNodeAddress, area: BoxAreaType) -> CSSPixelRectIterator;
326 fn query_client_rect(&self, node: TrustedNodeAddress) -> Rect<i32, CSSPixel>;
327 fn query_current_css_zoom(&self, node: TrustedNodeAddress) -> f32;
328 fn query_element_inner_outer_text(&self, node: TrustedNodeAddress) -> String;
329 fn query_offset_parent(&self, node: TrustedNodeAddress) -> OffsetParentResponse;
330 fn query_scroll_container(
333 &self,
334 node: Option<TrustedNodeAddress>,
335 flags: ScrollContainerQueryFlags,
336 ) -> Option<ScrollContainerResponse>;
337 fn query_resolved_style(
338 &self,
339 node: TrustedNodeAddress,
340 pseudo: Option<PseudoElement>,
341 property_id: PropertyId,
342 animations: DocumentAnimationSet,
343 animation_timeline_value: f64,
344 ) -> String;
345 fn query_resolved_font_style(
346 &self,
347 node: TrustedNodeAddress,
348 value: &str,
349 animations: DocumentAnimationSet,
350 animation_timeline_value: f64,
351 ) -> Option<ServoArc<Font>>;
352 fn query_scrolling_area(&self, node: Option<TrustedNodeAddress>) -> Rect<i32, CSSPixel>;
353 fn query_text_index(
355 &self,
356 node: TrustedNodeAddress,
357 point: Point2D<Au, CSSPixel>,
358 ) -> Option<usize>;
359 fn query_elements_from_point(
360 &self,
361 point: LayoutPoint,
362 flags: ElementsFromPointFlags,
363 ) -> Vec<ElementsFromPointResult>;
364 fn query_effective_overflow(&self, node: TrustedNodeAddress) -> Option<AxesOverflow>;
365 fn stylist_mut(&mut self) -> &mut Stylist;
366
367 fn set_accessibility_active(&self, active: bool);
368}
369
370pub trait ScriptThreadFactory {
374 fn create(
376 state: InitialScriptState,
377 layout_factory: Arc<dyn LayoutFactory>,
378 image_cache_factory: Arc<dyn ImageCacheFactory>,
379 background_hang_monitor_register: Box<dyn BackgroundHangMonitorRegister>,
380 ) -> JoinHandle<()>;
381}
382
383#[derive(Copy, Clone)]
386pub enum BoxAreaType {
387 Content,
388 Padding,
389 Border,
390}
391
392pub type CSSPixelRectIterator = Box<dyn Iterator<Item = Rect<Au, CSSPixel>>>;
393
394#[derive(Default)]
395pub struct PhysicalSides {
396 pub left: Au,
397 pub top: Au,
398 pub right: Au,
399 pub bottom: Au,
400}
401
402#[derive(Clone, Default)]
403pub struct OffsetParentResponse {
404 pub node_address: Option<UntrustedNodeAddress>,
405 pub rect: Rect<Au, CSSPixel>,
406}
407
408bitflags! {
409 #[derive(PartialEq)]
410 pub struct ScrollContainerQueryFlags: u8 {
411 const ForScrollParent = 1 << 0;
413 const Inclusive = 1 << 1;
415 }
416}
417
418#[derive(Clone, Copy, Debug, MallocSizeOf)]
419pub struct AxesOverflow {
420 pub x: Overflow,
421 pub y: Overflow,
422}
423
424impl Default for AxesOverflow {
425 fn default() -> Self {
426 Self {
427 x: Overflow::Visible,
428 y: Overflow::Visible,
429 }
430 }
431}
432
433impl From<&ComputedValues> for AxesOverflow {
434 fn from(style: &ComputedValues) -> Self {
435 Self {
436 x: style.clone_overflow_x(),
437 y: style.clone_overflow_y(),
438 }
439 }
440}
441
442impl AxesOverflow {
443 pub fn to_scrollable(&self) -> Self {
444 Self {
445 x: self.x.to_scrollable(),
446 y: self.y.to_scrollable(),
447 }
448 }
449
450 pub fn establishes_scroll_container(&self) -> bool {
452 self.x.is_scrollable()
455 }
456}
457
458#[derive(Clone)]
459pub enum ScrollContainerResponse {
460 Viewport(AxesOverflow),
461 Element(UntrustedNodeAddress, AxesOverflow),
462}
463
464#[derive(Debug, PartialEq)]
465pub enum QueryMsg {
466 BoxArea,
467 BoxAreas,
468 ClientRectQuery,
469 CurrentCSSZoomQuery,
470 EffectiveOverflow,
471 ElementInnerOuterTextQuery,
472 ElementsFromPoint,
473 InnerWindowDimensionsQuery,
474 NodesFromPointQuery,
475 OffsetParentQuery,
476 ScrollParentQuery,
477 ResolvedFontStyleQuery,
478 ResolvedStyleQuery,
479 ScrollingAreaOrOffsetQuery,
480 StyleQuery,
481 TextIndexQuery,
482 PaddingQuery,
483}
484
485#[derive(Debug, PartialEq)]
491pub enum ReflowGoal {
492 UpdateTheRendering,
495
496 LayoutQuery(QueryMsg),
499
500 UpdateScrollNode(ExternalScrollId, LayoutVector2D),
504}
505
506#[derive(Clone, Debug, MallocSizeOf)]
507pub struct IFrameSize {
508 pub browsing_context_id: BrowsingContextId,
509 pub pipeline_id: PipelineId,
510 pub viewport_details: ViewportDetails,
511}
512
513pub type IFrameSizes = FxHashMap<BrowsingContextId, IFrameSize>;
514
515bitflags! {
516 #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
519 pub struct RestyleReason: u16 {
520 const StylesheetsChanged = 1 << 0;
521 const DOMChanged = 1 << 1;
522 const PendingRestyles = 1 << 2;
523 const HighlightedDOMNodeChanged = 1 << 3;
524 const ThemeChanged = 1 << 4;
525 const ViewportChanged = 1 << 5;
526 const PaintWorkletLoaded = 1 << 6;
527 }
528}
529
530malloc_size_of_is_0!(RestyleReason);
531
532impl RestyleReason {
533 pub fn needs_restyle(&self) -> bool {
534 !self.is_empty()
535 }
536}
537
538#[derive(Debug, Default)]
540pub struct ReflowResult {
541 pub reflow_phases_run: ReflowPhasesRun,
543 pub reflow_statistics: ReflowStatistics,
544 pub pending_images: Vec<PendingImage>,
546 pub pending_rasterization_images: Vec<PendingRasterizationImage>,
548 pub pending_svg_elements_for_serialization: Vec<UntrustedNodeAddress>,
552 pub iframe_sizes: Option<IFrameSizes>,
558}
559
560bitflags! {
561 #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
563 pub struct ReflowPhasesRun: u8 {
564 const RanLayout = 1 << 0;
565 const CalculatedOverflow = 1 << 1;
566 const BuiltStackingContextTree = 1 << 2;
567 const BuiltDisplayList = 1 << 3;
568 const UpdatedScrollNodeOffset = 1 << 4;
569 const UpdatedImageData = 1 << 5;
573 }
574}
575
576impl ReflowPhasesRun {
577 pub fn needs_frame(&self) -> bool {
578 self.intersects(
579 Self::BuiltDisplayList | Self::UpdatedScrollNodeOffset | Self::UpdatedImageData,
580 )
581 }
582}
583
584#[derive(Debug, Default)]
585pub struct ReflowStatistics {
586 pub rebuilt_fragment_count: u32,
587 pub restyle_fragment_count: u32,
588}
589
590#[derive(Debug)]
593pub struct ReflowRequestRestyle {
594 pub reason: RestyleReason,
596 pub dirty_root: Option<TrustedNodeAddress>,
598 pub stylesheets_changed: bool,
600 pub pending_restyles: Vec<(TrustedNodeAddress, PendingRestyle)>,
602}
603
604#[derive(Debug)]
606pub struct ReflowRequest {
607 pub document: TrustedNodeAddress,
609 pub epoch: Epoch,
611 pub restyle: Option<ReflowRequestRestyle>,
613 pub viewport_details: ViewportDetails,
615 pub reflow_goal: ReflowGoal,
617 pub dom_count: u32,
619 pub origin: ImmutableOrigin,
621 pub animation_timeline_value: f64,
623 pub animations: DocumentAnimationSet,
625 pub animating_images: Arc<RwLock<AnimatingImages>>,
627 pub highlighted_dom_node: Option<OpaqueNode>,
629 pub document_context: WebFontDocumentContext,
631}
632
633impl ReflowRequest {
634 pub fn stylesheets_changed(&self) -> bool {
635 self.restyle
636 .as_ref()
637 .is_some_and(|restyle| restyle.stylesheets_changed)
638 }
639}
640
641#[derive(Debug, Default, MallocSizeOf)]
643pub struct PendingRestyle {
644 pub snapshot: Option<Snapshot>,
647
648 pub hint: RestyleHint,
650
651 pub damage: RestyleDamage,
653}
654
655#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, MallocSizeOf, PartialEq, Serialize)]
661pub enum FragmentType {
662 FragmentBody,
664 BeforePseudoContent,
666 AfterPseudoContent,
668}
669
670impl From<Option<PseudoElement>> for FragmentType {
671 fn from(value: Option<PseudoElement>) -> Self {
672 match value {
673 Some(PseudoElement::After) => FragmentType::AfterPseudoContent,
674 Some(PseudoElement::Before) => FragmentType::BeforePseudoContent,
675 _ => FragmentType::FragmentBody,
676 }
677 }
678}
679
680pub fn combine_id_with_fragment_type(id: usize, fragment_type: FragmentType) -> u64 {
681 debug_assert_eq!(id & (fragment_type as usize), 0);
682 (id as u64) | (fragment_type as u64)
683}
684
685pub fn node_id_from_scroll_id(id: usize) -> usize {
686 id & !3
687}
688
689#[derive(Clone, Debug, MallocSizeOf)]
690pub struct ImageAnimationState {
691 #[conditional_malloc_size_of]
692 pub image: Arc<RasterImage>,
693 pub active_frame: usize,
694 frame_start_time: f64,
695}
696
697impl ImageAnimationState {
698 pub fn new(image: Arc<RasterImage>, last_update_time: f64) -> Self {
699 Self {
700 image,
701 active_frame: 0,
702 frame_start_time: last_update_time,
703 }
704 }
705
706 pub fn image_key(&self) -> Option<ImageKey> {
707 self.image.id
708 }
709
710 pub fn duration_to_next_frame(&self, now: f64) -> Duration {
711 let frame_delay = self
712 .image
713 .frames
714 .get(self.active_frame)
715 .expect("Image frame should always be valid")
716 .delay
717 .unwrap_or_default();
718
719 let time_since_frame_start = (now - self.frame_start_time).max(0.0) * 1000.0;
720 let time_since_frame_start = Duration::from_secs_f64(time_since_frame_start);
721 frame_delay - time_since_frame_start.min(frame_delay)
722 }
723
724 pub fn update_frame_for_animation_timeline_value(&mut self, now: f64) -> bool {
728 if self.image.frames.len() <= 1 {
729 return false;
730 }
731 let image = &self.image;
732 let time_interval_since_last_update = now - self.frame_start_time;
733 let mut remain_time_interval = time_interval_since_last_update -
734 image
735 .frames
736 .get(self.active_frame)
737 .unwrap()
738 .delay()
739 .unwrap()
740 .as_secs_f64();
741 let mut next_active_frame_id = self.active_frame;
742 while remain_time_interval > 0.0 {
743 next_active_frame_id = (next_active_frame_id + 1) % image.frames.len();
744 remain_time_interval -= image
745 .frames
746 .get(next_active_frame_id)
747 .unwrap()
748 .delay()
749 .unwrap()
750 .as_secs_f64();
751 }
752 if self.active_frame == next_active_frame_id {
753 return false;
754 }
755 self.active_frame = next_active_frame_id;
756 self.frame_start_time = now;
757 true
758 }
759}
760
761#[derive(Debug)]
763pub struct ElementsFromPointResult {
764 pub node: OpaqueNode,
767 pub point_in_target: Point2D<f32, CSSPixel>,
770 pub cursor: Cursor,
773}
774
775bitflags! {
776 pub struct ElementsFromPointFlags: u8 {
777 const FindAll = 0b00000001;
780 }
781}
782
783#[derive(Debug, Default, MallocSizeOf)]
784pub struct AnimatingImages {
785 pub node_to_state_map: FxHashMap<OpaqueNode, ImageAnimationState>,
788 pub dirty: bool,
791}
792
793impl AnimatingImages {
794 pub fn maybe_insert_or_update(
795 &mut self,
796 node: OpaqueNode,
797 image: Arc<RasterImage>,
798 current_timeline_value: f64,
799 ) {
800 let entry = self.node_to_state_map.entry(node).or_insert_with(|| {
801 self.dirty = true;
802 ImageAnimationState::new(image.clone(), current_timeline_value)
803 });
804
805 if entry.image.id != image.id {
808 self.dirty = true;
809 *entry = ImageAnimationState::new(image.clone(), current_timeline_value);
810 }
811 }
812
813 pub fn remove(&mut self, node: OpaqueNode) {
814 if self.node_to_state_map.remove(&node).is_some() {
815 self.dirty = true;
816 }
817 }
818
819 pub fn clear_dirty(&mut self) -> bool {
821 std::mem::take(&mut self.dirty)
822 }
823
824 pub fn is_empty(&self) -> bool {
825 self.node_to_state_map.is_empty()
826 }
827}
828
829struct ThreadStateRestorer;
830
831impl ThreadStateRestorer {
832 fn new() -> Self {
833 #[cfg(debug_assertions)]
834 {
835 thread_state::exit(ThreadState::SCRIPT);
836 thread_state::enter(ThreadState::LAYOUT);
837 }
838 Self
839 }
840}
841
842impl Drop for ThreadStateRestorer {
843 fn drop(&mut self) {
844 #[cfg(debug_assertions)]
845 {
846 thread_state::exit(ThreadState::LAYOUT);
847 thread_state::enter(ThreadState::SCRIPT);
848 }
849 }
850}
851
852pub fn with_layout_state<R>(f: impl FnOnce() -> R) -> R {
858 let _guard = ThreadStateRestorer::new();
859 f()
860}
861
862#[cfg(test)]
863mod test {
864 use std::sync::Arc;
865 use std::time::Duration;
866
867 use pixels::{CorsStatus, ImageFrame, ImageMetadata, PixelFormat, RasterImage};
868
869 use crate::ImageAnimationState;
870
871 #[test]
872 fn test() {
873 let image_frames: Vec<ImageFrame> = std::iter::repeat_with(|| ImageFrame {
874 delay: Some(Duration::from_millis(100)),
875 byte_range: 0..1,
876 width: 100,
877 height: 100,
878 })
879 .take(10)
880 .collect();
881 let image = RasterImage {
882 metadata: ImageMetadata {
883 width: 100,
884 height: 100,
885 },
886 format: PixelFormat::BGRA8,
887 id: None,
888 bytes: Arc::new(vec![1]),
889 frames: image_frames,
890 cors_status: CorsStatus::Unsafe,
891 is_opaque: false,
892 };
893 let mut image_animation_state = ImageAnimationState::new(Arc::new(image), 0.0);
894
895 assert_eq!(image_animation_state.active_frame, 0);
896 assert_eq!(image_animation_state.frame_start_time, 0.0);
897 assert_eq!(
898 image_animation_state.update_frame_for_animation_timeline_value(0.101),
899 true
900 );
901 assert_eq!(image_animation_state.active_frame, 1);
902 assert_eq!(image_animation_state.frame_start_time, 0.101);
903 assert_eq!(
904 image_animation_state.update_frame_for_animation_timeline_value(0.116),
905 false
906 );
907 assert_eq!(image_animation_state.active_frame, 1);
908 assert_eq!(image_animation_state.frame_start_time, 0.101);
909 }
910}