paint: Add support for a blinking text caret (#43128)

This change adds support for a blinking text caret using WebRender
`DynamicProperties` updates. This ensures that text caret updates are as
lightweight as possible. As part of this change, the initial framework
for paint-side animations is added. Though this is driven by a timer for
now (in order to not have to run full speed animations for text carets),
the idea is that more animations can be driven by `Paint` in the future
(in order to avoid doing layouts during animations).

In addition, a preference is added which controls the caret blink speed.
This is used to disable caret blinking during WPT testing.

Testing: There are no tests for this change. We do not currently have a
good
way to test caret blinking as the caret is render only and fully testing
blinking would require well timed screenshot creation.
Fixes: #33237.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
Martin Robinson
2026-03-12 12:47:46 +01:00
committed by GitHub
parent a30bdc1870
commit ea995155f8
13 changed files with 315 additions and 20 deletions

View File

@@ -4,6 +4,7 @@
use std::env::consts::ARCH;
use std::sync::{RwLock, RwLockReadGuard};
use std::time::Duration;
use serde::{Deserialize, Serialize};
use servo_config_macro::ServoPreferences;
@@ -79,6 +80,9 @@ pub struct Preferences {
pub fonts_monospace: String,
pub fonts_default_size: i64,
pub fonts_default_monospace_size: i64,
/// The amount of time that a half cycle of a text caret blink takes in milliseconds.
/// If this value is less than or equal to zero, then caret blink is disabled.
pub editing_caret_blink_time: i64,
pub css_animations_testing_enabled: bool,
/// Start the devtools server at startup
pub devtools_server_enabled: bool,
@@ -323,6 +327,7 @@ impl Preferences {
const fn const_default() -> Self {
Self {
css_animations_testing_enabled: false,
editing_caret_blink_time: 600,
devtools_server_enabled: false,
devtools_server_listen_address: String::new(),
dom_abort_controller_enabled: true,
@@ -481,6 +486,16 @@ impl Preferences {
log_filter: String::new(),
}
}
/// The amount of time that a half cycle of a text caret blink takes. If blinking is disabled
/// this returns `None`.
pub fn editing_caret_blink_time(&self) -> Option<Duration> {
if self.editing_caret_blink_time > 0 {
Some(Duration::from_millis(self.editing_caret_blink_time as u64))
} else {
None
}
}
}
impl Default for Preferences {

View File

@@ -6,7 +6,7 @@ use std::cell::{OnceCell, RefCell};
use std::sync::Arc;
use app_units::{AU_PER_PX, Au};
use base::id::ScrollTreeNodeId;
use base::id::{PipelineId, ScrollTreeNodeId};
use clip::{Clip, ClipId};
use euclid::{Box2D, Point2D, Rect, Scale, SideOffsets2D, Size2D, UnknownUnit, Vector2D};
use fonts::GlyphStore;
@@ -16,7 +16,7 @@ use net_traits::image_cache::Image as CachedImage;
use paint_api::display_list::{PaintDisplayListInfo, SpatialTreeNodeInfo};
use servo_arc::Arc as ServoArc;
use servo_config::opts::DiagnosticsLogging;
use servo_config::pref;
use servo_config::{pref, prefs};
use servo_geometry::MaxRect;
use servo_url::ServoUrl;
use style::Zero;
@@ -45,7 +45,7 @@ use webrender_api::{
self as wr, BorderDetails, BorderRadius, BorderSide, BoxShadowClipMode, BuiltDisplayList,
ClipChainId, ClipMode, ColorF, CommonItemProperties, ComplexClipRegion, GlyphInstance,
NinePatchBorder, NinePatchBorderSource, NormalBorder, PrimitiveFlags, PropertyBinding,
SpatialId, SpatialTreeItemKey, units,
PropertyBindingKey, SpatialId, SpatialTreeItemKey, units,
};
use wr::units::LayoutVector2D;
@@ -211,6 +211,9 @@ impl DisplayListBuilder<'_> {
reflow_statistics,
};
// Clear any caret color from previous display list constructions.
builder.paint_info.caret_property_binding = None;
builder.add_all_spatial_nodes();
for clip in stacking_context_tree.clip_store.0.iter() {
@@ -1101,10 +1104,24 @@ impl Fragment {
ColorOrAuto::Auto => color,
};
let insertion_point_common = builder.common_properties(insertion_point_rect, &parent_style);
builder.wr().push_rect(
let caret_color = rgba(caret_color);
let property_binding = if prefs::get().editing_caret_blink_time().is_some() {
// It's okay to always use the same property binding key for this pipeline, as
// there is currently only a single thing that animates in this way (the caret).
// This code should be updated if we ever add more paint-side animations.
let pipeline_id: PipelineId = builder.paint_info.pipeline_id.into();
let property_binding_key = PropertyBindingKey::new(pipeline_id.into());
builder.paint_info.caret_property_binding = Some((property_binding_key, caret_color));
PropertyBinding::Binding(property_binding_key, caret_color)
} else {
PropertyBinding::Value(caret_color)
};
builder.wr().push_rect_with_animation(
&insertion_point_common,
insertion_point_rect,
rgba(caret_color),
property_binding,
);
}
}

View File

@@ -1182,6 +1182,7 @@ macro_rules! malloc_size_of_is_webrender_malloc_size_of(
);
);
malloc_size_of_is_webrender_malloc_size_of!(webrender::FastTransform<webrender_api::units::LayoutPixel, webrender_api::units::LayoutPixel>);
malloc_size_of_is_webrender_malloc_size_of!(webrender_api::BorderRadius);
malloc_size_of_is_webrender_malloc_size_of!(webrender_api::BorderStyle);
malloc_size_of_is_webrender_malloc_size_of!(webrender_api::BoxShadowClipMode);
@@ -1189,9 +1190,10 @@ malloc_size_of_is_webrender_malloc_size_of!(webrender_api::ColorF);
malloc_size_of_is_webrender_malloc_size_of!(webrender_api::Epoch);
malloc_size_of_is_webrender_malloc_size_of!(webrender_api::ExtendMode);
malloc_size_of_is_webrender_malloc_size_of!(webrender_api::ExternalScrollId);
malloc_size_of_is_webrender_malloc_size_of!(webrender_api::FontKey);
malloc_size_of_is_webrender_malloc_size_of!(webrender_api::FontInstanceFlags);
malloc_size_of_is_webrender_malloc_size_of!(webrender_api::FontInstanceKey);
malloc_size_of_is_webrender_malloc_size_of!(webrender_api::FontKey);
malloc_size_of_is_webrender_malloc_size_of!(webrender_api::FontVariation);
malloc_size_of_is_webrender_malloc_size_of!(webrender_api::GlyphInstance);
malloc_size_of_is_webrender_malloc_size_of!(webrender_api::GradientStop);
malloc_size_of_is_webrender_malloc_size_of!(webrender_api::ImageKey);
@@ -1200,13 +1202,14 @@ malloc_size_of_is_webrender_malloc_size_of!(webrender_api::LineStyle);
malloc_size_of_is_webrender_malloc_size_of!(webrender_api::MixBlendMode);
malloc_size_of_is_webrender_malloc_size_of!(webrender_api::NormalBorder);
malloc_size_of_is_webrender_malloc_size_of!(webrender_api::PipelineId);
malloc_size_of_is_webrender_malloc_size_of!(
webrender_api::PropertyBindingKey<webrender_api::ColorF>
);
malloc_size_of_is_webrender_malloc_size_of!(webrender_api::ReferenceFrameKind);
malloc_size_of_is_webrender_malloc_size_of!(webrender_api::RepeatMode);
malloc_size_of_is_webrender_malloc_size_of!(webrender_api::FontVariation);
malloc_size_of_is_webrender_malloc_size_of!(webrender_api::SpatialId);
malloc_size_of_is_webrender_malloc_size_of!(webrender_api::StickyOffsetBounds);
malloc_size_of_is_webrender_malloc_size_of!(webrender_api::TransformStyle);
malloc_size_of_is_webrender_malloc_size_of!(webrender::FastTransform<webrender_api::units::LayoutPixel,webrender_api::units::LayoutPixel>);
macro_rules! malloc_size_of_is_stylo_malloc_size_of(
($($ty:ty),+) => (

View File

@@ -30,6 +30,7 @@ mod refresh_driver;
mod render_notifier;
mod screenshot;
mod touch;
mod web_content_animation;
mod webrender_external_images;
mod webview_renderer;

View File

@@ -153,6 +153,8 @@ bitflags! {
const Resize = 1 << 3;
/// A fling has started and a repaint needs to happen to process the animation.
const StartedFlinging = 1 << 4;
/// A blinking text caret requires a redraw.
const BlinkingCaret = 1 << 5;
}
}

View File

@@ -2,7 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::cell::Cell;
use std::cell::{Cell, LazyCell};
use std::collections::hash_map::Entry;
use std::rc::Rc;
use std::sync::Arc;
@@ -47,10 +47,10 @@ use webrender_api::units::{
};
use webrender_api::{
self, BuiltDisplayList, BuiltDisplayListDescriptor, ColorF, DirtyRect, DisplayListPayload,
DocumentId, Epoch as WebRenderEpoch, ExternalScrollId, FontInstanceFlags, FontInstanceKey,
FontInstanceOptions, FontKey, FontVariation, ImageData, ImageKey, NativeFontHandle,
PipelineId as WebRenderPipelineId, PropertyBinding, ReferenceFrameKind, RenderReasons,
SampledScrollOffset, SpaceAndClipInfo, SpatialId, TransformStyle,
DocumentId, DynamicProperties, Epoch as WebRenderEpoch, ExternalScrollId, FontInstanceFlags,
FontInstanceKey, FontInstanceOptions, FontKey, FontVariation, ImageData, ImageKey,
NativeFontHandle, PipelineId as WebRenderPipelineId, PropertyBinding, ReferenceFrameKind,
RenderReasons, SampledScrollOffset, SpaceAndClipInfo, SpatialId, TransformStyle,
};
use wr_malloc_size_of::MallocSizeOfOps;
@@ -60,6 +60,7 @@ use crate::paint::{RepaintReason, WebRenderDebugOption};
use crate::refresh_driver::{AnimationRefreshDriverObserver, BaseRefreshDriver};
use crate::render_notifier::RenderNotifier;
use crate::screenshot::ScreenshotTaker;
use crate::web_content_animation::WebContentAnimator;
use crate::webrender_external_images::WebGLExternalImages;
use crate::webview_renderer::{PinchZoomResult, ScrollResult, UnknownWebView, WebViewRenderer};
@@ -127,6 +128,10 @@ pub(crate) struct Painter {
/// A cache that stores data for all animating images uploaded to WebRender. This is used
/// for animated images, which only need to update their offset in the data.
animation_image_cache: FxHashMap<ImageKey, Arc<Vec<u8>>>,
/// A [`WebContentAnimator`] used to manage web content-derived animations. Currently this only
/// manages blinking caret animations.
web_content_animator: WebContentAnimator,
}
impl Drop for Painter {
@@ -175,9 +180,11 @@ impl Painter {
WindowGLContext::initialize_image_handler(&mut external_image_handlers);
let embedder_to_constellation_sender = paint.embedder_to_constellation_sender.clone();
let timer_refresh_driver = LazyCell::default();
let refresh_driver = Rc::new(BaseRefreshDriver::new(
paint.event_loop_waker.clone_box(),
rendering_context.refresh_driver(),
&timer_refresh_driver,
));
let animation_refresh_driver_observer = Rc::new(AnimationRefreshDriverObserver::new(
embedder_to_constellation_sender.clone(),
@@ -272,6 +279,10 @@ impl Painter {
frame_delayer: Default::default(),
lcp_calculator: LargestContentfulPaintCalculator::new(),
animation_image_cache: FxHashMap::default(),
web_content_animator: WebContentAnimator::new(
paint.event_loop_waker.clone_box(),
(*timer_refresh_driver).clone(),
),
};
painter.assert_gl_framebuffer_complete();
painter.clear_background();
@@ -297,6 +308,18 @@ impl Painter {
.collect();
self.send_zoom_and_scroll_offset_updates(need_zoom, scroll_offset_updates);
if let Some(colors) = self.web_content_animator.update(&self.webview_renderers) {
let mut transaction = Transaction::new();
transaction.reset_dynamic_properties();
transaction.append_dynamic_properties(DynamicProperties {
transforms: Vec::new(),
floats: Vec::new(),
colors,
});
self.generate_frame(&mut transaction, RenderReasons::ANIMATED_PROPERTY);
self.send_transaction(transaction);
}
}
#[track_caller]
@@ -970,6 +993,11 @@ impl Painter {
.set(PaintMetricState::Seen(epoch, first_reflow));
}
details.animations.handle_new_display_list(
display_list_info.caret_property_binding,
&self.web_content_animator,
);
let mut transaction = Transaction::new();
let is_root_pipeline = Some(pipeline_id.into()) == webview_renderer.root_pipeline_id;
if is_root_pipeline && old_scale != webview_renderer.device_pixels_per_page_pixel() {

View File

@@ -13,6 +13,7 @@ use style_traits::CSSPixel;
use webrender_api::units::DevicePixel;
use crate::painter::PaintMetricState;
use crate::web_content_animation::PipelineAnimations;
pub(crate) struct PipelineDetails {
/// The pipeline associated with this PipelineDetails object.
@@ -21,6 +22,9 @@ pub(crate) struct PipelineDetails {
/// The id of the parent pipeline, if any.
pub parent_pipeline_id: Option<PipelineId>,
/// The ids of the child pipelines for this pipeline.
pub children: Vec<PipelineId>,
/// Whether animations are running
pub animations_running: bool,
@@ -55,6 +59,13 @@ pub(crate) struct PipelineDetails {
/// The [`Epoch`] of the latest display list received for this `Pipeline` or `None` if no
/// display list has been received.
pub display_list_epoch: Option<Epoch>,
/// Paint-driven animations associated with this [`PipelineDetails`]. Currently only text caret
/// is handled this way.
///
/// Note: This does not manage animations and transitions from CSS or for user input
/// interaction.
pub animations: PipelineAnimations,
}
impl PipelineDetails {
@@ -72,6 +83,7 @@ impl PipelineDetails {
PipelineDetails {
pipeline: None,
parent_pipeline_id: None,
children: Default::default(),
viewport_scale: None,
animations_running: false,
animation_callbacks_running: false,
@@ -82,6 +94,7 @@ impl PipelineDetails {
largest_contentful_paint_metric: Cell::new(PaintMetricState::Waiting),
exited: PipelineExitSource::empty(),
display_list_epoch: None,
animations: Default::default(),
}
}

View File

@@ -2,7 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::cell::{Cell, RefCell};
use std::cell::{Cell, LazyCell, RefCell};
use std::rc::Rc;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
@@ -40,9 +40,9 @@ impl BaseRefreshDriver {
pub(crate) fn new(
event_loop_waker: Box<dyn EventLoopWaker>,
refresh_driver: Option<Rc<dyn RefreshDriver>>,
timer_refresh_driver: &LazyCell<Rc<TimerRefreshDriver>>,
) -> Self {
let refresh_driver =
refresh_driver.unwrap_or_else(|| Rc::new(TimerRefreshDriver::default()));
let refresh_driver = refresh_driver.unwrap_or_else(|| (**timer_refresh_driver).clone());
Self {
waiting_for_frame: Arc::new(AtomicBool::new(false)),
event_loop_waker,
@@ -193,7 +193,7 @@ enum TimerThreadMessage {
/// It would be nice to integrate this somehow into the embedder thread, but it would
/// require both some communication with the embedder and for all embedders to be well
/// behave respecting wakeup timeouts -- a bit too much to ask at the moment.
struct TimerRefreshDriver {
pub(crate) struct TimerRefreshDriver {
sender: Sender<TimerThreadMessage>,
join_handle: Option<JoinHandle<()>>,
}
@@ -232,7 +232,7 @@ impl Default for TimerRefreshDriver {
}
impl TimerRefreshDriver {
fn queue_timer(&self, duration: Duration, callback: BoxedTimerCallback) {
pub(crate) fn queue_timer(&self, duration: Duration, callback: BoxedTimerCallback) {
let _ = self
.sender
.send(TimerThreadMessage::Request(TimerEventRequest {

View File

@@ -0,0 +1,179 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::cell::{Cell, RefCell};
use std::rc::Rc;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use base::id::WebViewId;
use embedder_traits::EventLoopWaker;
use rustc_hash::FxHashMap;
use servo_config::prefs;
use webrender_api::{ColorF, PropertyBindingKey, PropertyValue};
use crate::refresh_driver::TimerRefreshDriver;
use crate::webview_renderer::WebViewRenderer;
/// The amount of the time the caret blinks before ceasing, in order to preserve power. User
/// activity (a new display list) will reset this.
///
/// TODO: This should be controlled by system settings.
pub(crate) const CARET_BLINK_TIMEOUT: Duration = Duration::from_secs(30);
/// A struct responsible for managing paint-side animations. Currently this only handles text caret
/// blinking, but the idea is that in the future this would handle other types of paint-side
/// animations as well.
///
/// Note: This does not control animations requiring layout (all CSS transitions and animations
/// currently) nor animations due to touch events such as fling.
pub(crate) struct WebContentAnimator {
event_loop_waker: Box<dyn EventLoopWaker>,
timer_refresh_driver: Rc<TimerRefreshDriver>,
caret_visible: Cell<bool>,
timer_scheduled: Cell<bool>,
need_update: Arc<AtomicBool>,
}
impl WebContentAnimator {
pub(crate) fn new(
event_loop_waker: Box<dyn EventLoopWaker>,
timer_refresh_driver: Rc<TimerRefreshDriver>,
) -> Self {
Self {
event_loop_waker,
timer_refresh_driver,
caret_visible: Cell::new(true),
timer_scheduled: Default::default(),
need_update: Default::default(),
}
}
pub(crate) fn schedule_timer_if_necessary(&self) {
if self.timer_scheduled.get() {
return;
}
let Some(caret_blink_time) = prefs::get().editing_caret_blink_time() else {
return;
};
let event_loop_waker = self.event_loop_waker.clone();
let need_update = self.need_update.clone();
self.timer_refresh_driver.queue_timer(
caret_blink_time,
Box::new(move || {
need_update.store(true, Ordering::Relaxed);
event_loop_waker.wake();
}),
);
self.timer_scheduled.set(true);
}
pub(crate) fn update(
&self,
webview_renderers: &FxHashMap<WebViewId, WebViewRenderer>,
) -> Option<Vec<PropertyValue<ColorF>>> {
if !self.need_update.load(Ordering::Relaxed) {
return None;
}
let mut colors = Vec::new();
for renderer in webview_renderers.values() {
renderer.for_each_connected_pipeline(&mut |pipeline_details| {
if let Some(property_value) =
pipeline_details.animations.update(self.caret_visible.get())
{
colors.push(property_value);
}
});
}
self.timer_scheduled.set(false);
self.need_update.store(false, Ordering::Relaxed);
if colors.is_empty() {
// All animations have stopped. When a new blinking caret is activated we want
// it to start in the visible state, so we set `caret_visible` to true here.
self.caret_visible.set(true);
return None;
}
self.caret_visible.set(!self.caret_visible.get());
self.schedule_timer_if_necessary();
Some(colors)
}
}
/// This structure tracks the animations active for a given pipeline. Currently only caret
/// blinking is tracked, but in the future this could perhaps track paint-side animations.
#[derive(Default)]
pub(crate) struct PipelineAnimations {
caret: RefCell<Option<CaretAnimation>>,
}
impl PipelineAnimations {
pub(crate) fn update(&self, caret_visible: bool) -> Option<PropertyValue<ColorF>> {
let mut maybe_caret = self.caret.borrow_mut();
let caret = maybe_caret.as_mut()?;
if let Some(update) = caret.update(caret_visible) {
return Some(update);
}
*maybe_caret = None;
None
}
pub(crate) fn handle_new_display_list(
&self,
caret_property_binding: Option<(PropertyBindingKey<ColorF>, ColorF)>,
web_content_animator: &WebContentAnimator,
) {
let Some(caret_blink_time) = prefs::get().editing_caret_blink_time() else {
return;
};
*self.caret.borrow_mut() = match caret_property_binding {
Some((caret_property_key, original_caret_color)) => {
web_content_animator.schedule_timer_if_necessary();
Some(CaretAnimation {
caret_property_key,
original_caret_color,
remaining_blink_count: (CARET_BLINK_TIMEOUT.as_millis() /
caret_blink_time.as_millis())
as usize,
})
},
None => None,
}
}
}
/// Tracks the state of an ongoing caret blinking animation.
struct CaretAnimation {
pub caret_property_key: PropertyBindingKey<ColorF>,
pub original_caret_color: ColorF,
pub remaining_blink_count: usize,
}
impl CaretAnimation {
pub(crate) fn update(&mut self, caret_visible: bool) -> Option<PropertyValue<ColorF>> {
if self.remaining_blink_count == 0 {
return None;
}
self.remaining_blink_count = self.remaining_blink_count.saturating_sub(1);
let value = if caret_visible || self.remaining_blink_count == 0 {
self.original_caret_color
} else {
ColorF::TRANSPARENT
};
Some(PropertyValue {
key: self.caret_property_key,
value,
})
}
}

View File

@@ -270,6 +270,11 @@ impl WebViewRenderer {
let pipeline_details = self.ensure_pipeline_details(pipeline_id);
pipeline_details.pipeline = Some(frame_tree.pipeline.clone());
pipeline_details.parent_pipeline_id = parent_pipeline_id;
pipeline_details.children = frame_tree
.children
.iter()
.map(|frame_tree| frame_tree.pipeline.id)
.collect();
for kid in &frame_tree.children {
self.set_frame_tree_on_pipeline_details(kid, Some(pipeline_id));
@@ -333,6 +338,26 @@ impl WebViewRenderer {
self.webview.set_animating(self.animating());
}
pub(crate) fn for_each_connected_pipeline(&self, callback: &mut impl FnMut(&PipelineDetails)) {
if let Some(root_pipeline_id) = self.root_pipeline_id {
self.for_each_connected_pipeline_internal(root_pipeline_id, callback);
}
}
fn for_each_connected_pipeline_internal(
&self,
pipeline_id: PipelineId,
callback: &mut impl FnMut(&PipelineDetails),
) {
let Some(pipeline) = self.pipelines.get(&pipeline_id) else {
return;
};
callback(pipeline);
for child_pipeline_id in &pipeline.children {
self.for_each_connected_pipeline_internal(*child_pipeline_id, callback);
}
}
/// Update touch-based animations (currently just fling) during a `RefreshDriver`-based
/// frame tick. Returns `true` if we should continue observing frames (the fling is ongoing)
/// or `false` if we should stop observing frames (the fling has finished).

View File

@@ -279,6 +279,12 @@ impl From<PipelineId> for TreeId {
}
}
impl From<PipelineId> for u64 {
fn from(pipeline_id: PipelineId) -> Self {
((pipeline_id.namespace_id.0 as u64) << 32) + pipeline_id.index.0.get() as u64
}
}
#[cfg(test)]
#[test]
fn test_pipeline_id_to_accesskit_tree_id() {

View File

@@ -20,8 +20,8 @@ use servo_geometry::FastLayoutTransform;
use style::values::specified::Overflow;
use webrender_api::units::{LayoutPixel, LayoutPoint, LayoutRect, LayoutSize, LayoutVector2D};
use webrender_api::{
ExternalScrollId, PipelineId, ReferenceFrameKind, ScrollLocation, SpatialId,
StickyOffsetBounds, TransformStyle,
ColorF, ExternalScrollId, PipelineId, PropertyBindingKey, ReferenceFrameKind, ScrollLocation,
SpatialId, StickyOffsetBounds, TransformStyle,
};
/// A scroll type, describing whether what kind of action originated this scroll request.
@@ -850,6 +850,10 @@ pub struct PaintDisplayListInfo {
/// Whether the first layout or a subsequent (incremental) layout triggered this
/// display list creation.
pub first_reflow: bool,
/// If this display list contains a blinking caret, this value will be filled with its animation
/// key and original color value so that the painter can animate the caret.
pub caret_property_binding: Option<(PropertyBindingKey<ColorF>, ColorF)>,
}
impl PaintDisplayListInfo {
@@ -899,6 +903,7 @@ impl PaintDisplayListInfo {
root_scroll_node_id,
is_contentful: false,
first_reflow,
caret_property_binding: Default::default(),
}
}

View File

@@ -1,5 +1,6 @@
{
"dom_webxr_test": true,
"editing_caret_blink_time": 0,
"gfx_text_antialiasing_enabled": false,
"dom_testutils_enabled": true
}