mirror of
https://github.com/lexmount/moli.git
synced 2026-10-13 00:01:13 +00:00
fix(browser): address activation and document review regressions
This commit is contained in:
@@ -70,22 +70,23 @@ where
|
||||
/// longer fit. Retained entries keep their original insertion order.
|
||||
pub fn set_limits(&mut self, limits: ByteLimits) -> Vec<(K, V)> {
|
||||
self.limits = limits;
|
||||
let oversized = self
|
||||
.insertion_order
|
||||
.iter()
|
||||
.filter(|key| {
|
||||
self.entries
|
||||
.get(*key)
|
||||
.is_some_and(|entry| entry.byte_len > limits.max_entry_bytes)
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
let mut evicted = Vec::new();
|
||||
for key in oversized {
|
||||
if let Some(value) = self.remove(&key) {
|
||||
evicted.push((key, value));
|
||||
// Filter the order once. Calling remove for every oversized entry
|
||||
// would rescan the whole queue on each removal when budgets shrink.
|
||||
self.insertion_order.retain(|key| {
|
||||
if self
|
||||
.entries
|
||||
.get(key)
|
||||
.is_none_or(|entry| entry.byte_len <= limits.max_entry_bytes)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if let Some(entry) = self.entries.remove(key) {
|
||||
self.used_bytes -= entry.byte_len;
|
||||
evicted.push((key.clone(), entry.value));
|
||||
}
|
||||
false
|
||||
});
|
||||
while self.used_bytes > limits.max_total_bytes {
|
||||
if let Some(entry) = self.pop_oldest() {
|
||||
evicted.push(entry);
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
use super::{BoundedByteBuffer, ByteLimits, InsertOutcome};
|
||||
|
||||
#[test]
|
||||
fn shrinking_entry_limit_keeps_interleaved_survivors_in_fifo_order() {
|
||||
let mut buffer = BoundedByteBuffer::new(ByteLimits::new(100, 20));
|
||||
for (key, bytes) in [(0, 10), (1, 1), (2, 10), (3, 1), (4, 10)] {
|
||||
buffer.insert(key, key, bytes);
|
||||
}
|
||||
assert_eq!(
|
||||
buffer.set_limits(ByteLimits::new(100, 1)),
|
||||
vec![(0, 0), (2, 2), (4, 4)]
|
||||
);
|
||||
assert_eq!(buffer.used_bytes(), 2);
|
||||
assert_eq!(buffer.set_limits(ByteLimits::new(1, 1)), vec![(1, 1)]);
|
||||
assert_eq!(buffer.get(&3), Some(&3));
|
||||
assert_eq!(buffer.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn changing_limits_evicts_oversized_then_oldest_without_reordering_survivors() {
|
||||
let mut buffer = BoundedByteBuffer::new(ByteLimits::new(20, 10));
|
||||
|
||||
@@ -573,14 +573,39 @@ pub(crate) fn build_inline_fragments(
|
||||
.iter()
|
||||
.map(text_style_paint_outsets)
|
||||
.collect::<Vec<_>>();
|
||||
// Inline objects occupy positions between text units, not text bytes.
|
||||
// Retain their logical offsets so spaces before an atomic box cannot be
|
||||
// mistaken for line-end whitespace (including in bidi-reordered lines).
|
||||
let atomic_offsets: HashMap<_, _> = layout
|
||||
.inline_boxes()
|
||||
.filter(|inline_box| {
|
||||
context
|
||||
.object(inline_box.id)
|
||||
.is_some_and(|object| object.role == InlineObjectRole::Atomic)
|
||||
})
|
||||
.map(|inline_box| (inline_box.id, inline_box.index))
|
||||
.collect();
|
||||
|
||||
for (line_index, line) in layout.lines().enumerate() {
|
||||
let metrics = line.metrics();
|
||||
let line_range = line.text_range();
|
||||
let last_atomic_offset = line
|
||||
.items()
|
||||
.filter_map(|item| match item {
|
||||
PositionedLayoutItem::InlineBox(positioned) => {
|
||||
atomic_offsets.get(&positioned.id).copied()
|
||||
}
|
||||
PositionedLayoutItem::GlyphRun(_) => None,
|
||||
})
|
||||
.max()
|
||||
.unwrap_or(line_range.start);
|
||||
let trailing_start = overlapping_output_ranges(&context.text_units, &line_range)
|
||||
.iter()
|
||||
.rev()
|
||||
.take_while(|unit| unit.control || unit.collapsible_whitespace)
|
||||
.take_while(|unit| {
|
||||
unit.output_range.start >= last_atomic_offset
|
||||
&& (unit.control || unit.collapsible_whitespace)
|
||||
})
|
||||
.filter(|unit| unit.collapsible_whitespace)
|
||||
.map(|unit| unit.output_range.start)
|
||||
.last();
|
||||
|
||||
@@ -773,37 +773,7 @@ where
|
||||
let bottom =
|
||||
last.final_layout.location.y - last_offset.y + last.final_layout.size.height;
|
||||
let rect = LayoutRect::new(content.x, top, content.width, (bottom - top).max(0.0));
|
||||
// A zero-area leading continuation follows escaped leading
|
||||
// margins into the first block's flow position. It carries no
|
||||
// line strut of its own and must not remain at the pre-collapse y.
|
||||
for id in &self.boxes[run.preceding_inline.index()].fragments {
|
||||
let existing = &mut self.fragments[id.index()];
|
||||
if matches!(existing.kind, LayoutFragmentKind::InlineBox { .. })
|
||||
&& existing.rect.width == 0.0
|
||||
&& existing.rect.height == 0.0
|
||||
{
|
||||
let original = self.coordinate_spaces[existing.coordinate_space.index()]
|
||||
.layout
|
||||
.local_to_document_ignoring_css_transforms
|
||||
.map_point(LayoutPoint::new(existing.rect.x, existing.rect.y));
|
||||
let x = self.coordinate_spaces[parent.index() + 1]
|
||||
.layout
|
||||
.local_to_document_ignoring_css_transforms
|
||||
.inverse()
|
||||
.map(|inverse| inverse.map_point(original).x)
|
||||
.unwrap_or(existing.rect.x);
|
||||
let boundary = LayoutRect::new(x, top, 0.0, 0.0);
|
||||
existing.rect = boundary;
|
||||
existing.coordinate_space =
|
||||
LayoutCoordinateSpaceId::from_index(parent.index() + 1);
|
||||
existing.box_model = Some(LayoutFragmentBoxModel {
|
||||
content: boundary,
|
||||
padding: boundary,
|
||||
border: boundary,
|
||||
margin: boundary,
|
||||
});
|
||||
}
|
||||
}
|
||||
self.reposition_empty_inline_continuation(run.preceding_inline, parent, top);
|
||||
let fragment = self.push_fragment(LayoutFragment {
|
||||
id: LayoutFragmentId::from_index(0),
|
||||
kind: LayoutFragmentKind::Box {
|
||||
@@ -824,6 +794,46 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// An empty leading continuation follows escaped margins into the first
|
||||
/// block's flow position; it carries no independent line strut.
|
||||
fn reposition_empty_inline_continuation(
|
||||
&mut self,
|
||||
inline: LayoutBoxId,
|
||||
parent: LayoutBoxId,
|
||||
top: f32,
|
||||
) {
|
||||
let coordinate_space = LayoutCoordinateSpaceId::from_index(parent.index() + 1);
|
||||
let document_to_parent = self.coordinate_spaces[coordinate_space.index()]
|
||||
.layout
|
||||
.local_to_document_ignoring_css_transforms
|
||||
.inverse();
|
||||
for id in &self.boxes[inline.index()].fragments {
|
||||
let fragment = &mut self.fragments[id.index()];
|
||||
if !matches!(fragment.kind, LayoutFragmentKind::InlineBox { .. })
|
||||
|| fragment.rect.width != 0.0
|
||||
|| fragment.rect.height != 0.0
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let original = self.coordinate_spaces[fragment.coordinate_space.index()]
|
||||
.layout
|
||||
.local_to_document_ignoring_css_transforms
|
||||
.map_point(LayoutPoint::new(fragment.rect.x, fragment.rect.y));
|
||||
let x = document_to_parent
|
||||
.map(|inverse| inverse.map_point(original).x)
|
||||
.unwrap_or(fragment.rect.x);
|
||||
let boundary = LayoutRect::new(x, top, 0.0, 0.0);
|
||||
fragment.rect = boundary;
|
||||
fragment.coordinate_space = coordinate_space;
|
||||
fragment.box_model = Some(LayoutFragmentBoxModel {
|
||||
content: boundary,
|
||||
padding: boundary,
|
||||
border: boundary,
|
||||
margin: boundary,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn split_block_relative_inset_basis(&self, parent: LayoutBoxId) -> taffy::Size<Option<f32>> {
|
||||
let content = self.boxes[parent.index()].content_box;
|
||||
let Some(inputs) = self
|
||||
|
||||
@@ -103,45 +103,44 @@ impl PreparedWorldLayout {
|
||||
N: Copy + Debug + Eq + Hash,
|
||||
{
|
||||
assert!(self.feedback_invalidation_worklist.is_empty());
|
||||
if viewport_changed {
|
||||
for layout_box in &mut world.boxes {
|
||||
layout_box.clear_layout_caches();
|
||||
}
|
||||
world.viewport_layout.cache.clear();
|
||||
return world.boxes.len();
|
||||
}
|
||||
if changed_boxes.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
self.feedback_invalidation_marks
|
||||
.resize(world.boxes.len(), false);
|
||||
let mut invalidated = Vec::new();
|
||||
if viewport_changed {
|
||||
for index in 0..world.boxes.len() {
|
||||
let id = LayoutBoxId::from_index(index);
|
||||
self.feedback_invalidation_marks[index] = true;
|
||||
invalidated.push(id);
|
||||
world.boxes[index].clear_layout_caches();
|
||||
self.feedback_invalidation_worklist
|
||||
.extend(changed_boxes.iter().copied());
|
||||
while let Some(id) = self.feedback_invalidation_worklist.pop() {
|
||||
if self.feedback_invalidation_marks[id.index()] {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
self.feedback_invalidation_marks[id.index()] = true;
|
||||
invalidated.push(id);
|
||||
world.boxes[id.index()].clear_layout_caches();
|
||||
self.feedback_invalidation_worklist
|
||||
.extend(changed_boxes.iter().copied());
|
||||
while let Some(id) = self.feedback_invalidation_worklist.pop() {
|
||||
if self.feedback_invalidation_marks[id.index()] {
|
||||
continue;
|
||||
}
|
||||
self.feedback_invalidation_marks[id.index()] = true;
|
||||
invalidated.push(id);
|
||||
world.boxes[id.index()].clear_layout_caches();
|
||||
self.feedback_invalidation_worklist
|
||||
.extend(world.boxes[id.index()].layout_children.iter().copied());
|
||||
}
|
||||
.extend(world.boxes[id.index()].layout_children.iter().copied());
|
||||
}
|
||||
|
||||
for changed in changed_boxes.iter().copied() {
|
||||
let mut ancestor = world.boxes[changed.index()].layout_parent;
|
||||
while let Some(id) = ancestor {
|
||||
if !self.feedback_invalidation_marks[id.index()] {
|
||||
self.feedback_invalidation_marks[id.index()] = true;
|
||||
invalidated.push(id);
|
||||
world.boxes[id.index()].clear_layout_caches();
|
||||
}
|
||||
ancestor = world.boxes[id.index()].layout_parent;
|
||||
for changed in changed_boxes.iter().copied() {
|
||||
let mut ancestor = world.boxes[changed.index()].layout_parent;
|
||||
while let Some(id) = ancestor {
|
||||
if !self.feedback_invalidation_marks[id.index()] {
|
||||
self.feedback_invalidation_marks[id.index()] = true;
|
||||
invalidated.push(id);
|
||||
world.boxes[id.index()].clear_layout_caches();
|
||||
}
|
||||
ancestor = world.boxes[id.index()].layout_parent;
|
||||
}
|
||||
}
|
||||
if viewport_changed || !changed_boxes.is_empty() {
|
||||
world.viewport_layout.cache.clear();
|
||||
}
|
||||
world.viewport_layout.cache.clear();
|
||||
for id in invalidated.iter().copied() {
|
||||
self.feedback_invalidation_marks[id.index()] = false;
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@ use moli_layout::{
|
||||
LayoutElementSemantics, LayoutEnvironment, LayoutError, LayoutFlushReason, LayoutFragmentKind,
|
||||
LayoutNamespace, LayoutPaintedSurfaceHit, LayoutPassRequest, LayoutPassResult, LayoutPoint,
|
||||
LayoutPosition, LayoutQuery, LayoutQueryAnswer, LayoutQueryBatch, LayoutRect,
|
||||
LayoutScrollbarColors, LayoutScrollbarGutter, LayoutScrollbarPart, LayoutScrollbarWidth,
|
||||
LayoutSource, LayoutSourceKind, LayoutStyleResolver, LayoutTransform2D, LayoutViewport,
|
||||
PaintBrush, PaintCaptureRequest, PaintColor, PaintFragment, PaintShape,
|
||||
LayoutReplacedKind, LayoutScrollbarColors, LayoutScrollbarGutter, LayoutScrollbarPart,
|
||||
LayoutScrollbarWidth, LayoutSource, LayoutSourceKind, LayoutStyleResolver, LayoutTransform2D,
|
||||
LayoutViewport, PaintBrush, PaintCaptureRequest, PaintColor, PaintFragment, PaintShape,
|
||||
ResolvedLayoutElementStyles, ResolvedLayoutStyle, build_layout_pass,
|
||||
};
|
||||
use style::Atom;
|
||||
@@ -91,7 +91,7 @@ impl LayoutSource for Source {
|
||||
LayoutNamespace::Html,
|
||||
self.0[node].local_name,
|
||||
LayoutElementCategory::Generic,
|
||||
None,
|
||||
(self.0[node].local_name == "img").then_some(LayoutReplacedKind::Image),
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -2809,6 +2809,49 @@ fn inline_output_preserves_line_text_and_utf16_source_fragments() {
|
||||
assert_eq!(scroll_geometry.scroll_containers.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collapsed_space_before_atomic_inline_keeps_its_range_geometry() {
|
||||
for local_name in ["span", "img"] {
|
||||
for trailing_text in ["", " "] {
|
||||
let source = Source(vec![
|
||||
Node::element("root", vec![1, 2, 3]),
|
||||
Node::text("before", "a "),
|
||||
Node::html_element("atomic", local_name, vec![]),
|
||||
Node::text("after", trailing_text),
|
||||
]);
|
||||
let mut styles = Styles::default();
|
||||
styles
|
||||
.0
|
||||
.insert(0, fixed_size(LayoutDisplay::Block, 200.0, 80.0));
|
||||
for node in [1, 3] {
|
||||
styles
|
||||
.0
|
||||
.insert(node, resolved(LayoutDisplay::Inline, Style::default()));
|
||||
}
|
||||
let display = if local_name == "img" {
|
||||
LayoutDisplay::Inline
|
||||
} else {
|
||||
LayoutDisplay::InlineBlock
|
||||
};
|
||||
styles.0.insert(2, fixed_size(display, 20.0, 20.0));
|
||||
let output = build(&source, &mut styles);
|
||||
let space = output.text_range_rects(1, 1..2);
|
||||
assert_eq!(space.len(), 1, "{local_name}, {trailing_text:?}");
|
||||
let space = space[0].bounding_rect();
|
||||
assert!(
|
||||
space.width > 0.0,
|
||||
"space before an atomic inline is not trailing"
|
||||
);
|
||||
let atomic = output.client_rects_for_source(2)[0].bounding_rect();
|
||||
assert_close(space.x + space.width, atomic.x);
|
||||
assert!(
|
||||
output.text_range_rects(3, 0..1).is_empty(),
|
||||
"line-end space still collapses"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inline_offset_metrics_use_fragment_bounds_and_keep_empty_sibling_anchors() {
|
||||
let source = Source(vec![
|
||||
|
||||
@@ -1391,24 +1391,39 @@ impl CapturedResponseBodyStore {
|
||||
self.durable_sessions.insert(session, limits);
|
||||
} else {
|
||||
self.durable_sessions.remove(&session);
|
||||
// Turning off durability revokes only old-document claims, not
|
||||
// ordinary access to responses from the current document.
|
||||
for request_id in self.retained_request_ids.clone() {
|
||||
if let Some(body) = self.bodies.get_mut(&request_id)
|
||||
&& !body.remove_session_visibility(session_id)
|
||||
{
|
||||
self.bodies.remove(&request_id);
|
||||
}
|
||||
if let Some(body) = self.buffered_bodies.get_mut(&request_id)
|
||||
&& !body.remove_session_visibility(session_id)
|
||||
{
|
||||
self.buffered_bodies.remove(&request_id);
|
||||
}
|
||||
self.revoke_retained_visibility(session_id);
|
||||
}
|
||||
for (id, mut body) in self
|
||||
.buffered_bodies
|
||||
.set_limits(self.effective_durable_limits())
|
||||
{
|
||||
body.mark_evicted();
|
||||
self.bodies.insert(id, body);
|
||||
}
|
||||
self.synchronize_durable_entries();
|
||||
}
|
||||
|
||||
fn revoke_retained_visibility(&mut self, session_id: Option<&str>) {
|
||||
// Revocation affects only old-document claims, not ordinary access to
|
||||
// responses from the current document.
|
||||
for request_id in &self.retained_request_ids {
|
||||
if let Some(body) = self.bodies.get_mut(request_id)
|
||||
&& !body.remove_session_visibility(session_id)
|
||||
{
|
||||
self.bodies.remove(request_id);
|
||||
}
|
||||
if let Some(body) = self.buffered_bodies.get_mut(request_id)
|
||||
&& !body.remove_session_visibility(session_id)
|
||||
{
|
||||
self.buffered_bodies.remove(request_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn effective_durable_limits(&self) -> ByteLimits {
|
||||
// One target owns one payload store. Multiple clients cannot multiply
|
||||
// its budget; each requested budget is an upper bound, not a reserve.
|
||||
let limits = self.durable_sessions.values().fold(
|
||||
self.durable_sessions.values().fold(
|
||||
ByteLimits::new(
|
||||
RESPONSE_BODY_BUFFER_MAX_TOTAL_BYTES,
|
||||
RESPONSE_BODY_BUFFER_MAX_ENTRY_BYTES,
|
||||
@@ -1419,26 +1434,26 @@ impl CapturedResponseBodyStore {
|
||||
acc.max_entry_bytes.min(limits.max_entry_bytes),
|
||||
)
|
||||
},
|
||||
);
|
||||
for (id, mut body) in self.buffered_bodies.set_limits(limits) {
|
||||
body.mark_evicted();
|
||||
self.bodies.insert(id, body);
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fn synchronize_durable_entries(&mut self) {
|
||||
if self.durable_sessions.is_empty() {
|
||||
self.durable_entry_order.clear();
|
||||
self.retained_request_ids.clear();
|
||||
} else {
|
||||
let mut untracked = self
|
||||
.bodies
|
||||
.keys()
|
||||
.chain(self.buffered_bodies.iter().map(|(id, _)| id))
|
||||
.filter(|id| !self.durable_entry_order.contains(id))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
untracked.sort();
|
||||
self.durable_entry_order.extend(untracked);
|
||||
self.trim_durable_entries();
|
||||
return;
|
||||
}
|
||||
let tracked: HashSet<_> = self.durable_entry_order.iter().collect();
|
||||
let mut untracked = self
|
||||
.bodies
|
||||
.keys()
|
||||
.chain(self.buffered_bodies.iter().map(|(id, _)| id))
|
||||
.filter(|id| !tracked.contains(id))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
untracked.sort();
|
||||
self.durable_entry_order.extend(untracked);
|
||||
self.trim_durable_entries();
|
||||
}
|
||||
|
||||
fn track_durable_entry(&mut self, request_id: &str) {
|
||||
|
||||
@@ -489,10 +489,12 @@ fn spawn_repeated_synchronous_xhr_server(
|
||||
}
|
||||
Err(error) => panic!("accept repeated synchronous XHR: {error}"),
|
||||
};
|
||||
// Accepted sockets inherit nonblocking mode on macOS.
|
||||
// macOS can inherit O_NONBLOCK from the listening socket.
|
||||
// This fixture reads the complete request synchronously; the
|
||||
// socket timeout alone does not turn WouldBlock into a wait.
|
||||
stream
|
||||
.set_nonblocking(false)
|
||||
.expect("use blocking synchronous XHR request I/O");
|
||||
.expect("make accepted synchronous XHR stream blocking");
|
||||
stream
|
||||
.set_read_timeout(Some(Duration::from_secs(2)))
|
||||
.expect("bound synchronous XHR request read");
|
||||
|
||||
Reference in New Issue
Block a user