initial macos porting work
This commit is contained in:
@@ -128,7 +128,12 @@ impl LayoutNode {
|
||||
/// Return a copy of this node (and all descendants) with all positions shifted by `offset`.
|
||||
pub fn translated(self, offset: Point) -> Self {
|
||||
fn translate_rect(r: Rect, o: Point) -> Rect {
|
||||
Rect::new(r.origin.x + o.x, r.origin.y + o.y, r.size.width, r.size.height)
|
||||
Rect::new(
|
||||
r.origin.x + o.x,
|
||||
r.origin.y + o.y,
|
||||
r.size.width,
|
||||
r.size.height,
|
||||
)
|
||||
}
|
||||
let rect = translate_rect(self.rect, offset);
|
||||
let clip_rect = self.clip_rect.map(|r| translate_rect(r, offset));
|
||||
@@ -143,7 +148,11 @@ impl LayoutNode {
|
||||
rect: translate_rect(img.rect, offset),
|
||||
..img
|
||||
});
|
||||
let children = self.children.into_iter().map(|c| c.translated(offset)).collect();
|
||||
let children = self
|
||||
.children
|
||||
.into_iter()
|
||||
.map(|c| c.translated(offset))
|
||||
.collect();
|
||||
LayoutNode {
|
||||
rect,
|
||||
clip_rect,
|
||||
@@ -241,6 +250,7 @@ pub fn layout_snapshot_with_cache(
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_element(
|
||||
element: &Element,
|
||||
rect: Rect,
|
||||
@@ -256,24 +266,24 @@ fn layout_element(
|
||||
perf_stats.nodes += 1;
|
||||
|
||||
// Viewport culling: skip fully off-screen elements inside a scroll box.
|
||||
if let Some(clip) = clip_rect {
|
||||
if !rects_overlap(rect, clip) {
|
||||
perf_stats.viewport_culled += 1;
|
||||
return LayoutNode {
|
||||
path,
|
||||
element_id: element.id,
|
||||
rect,
|
||||
corner_radius: 0.0,
|
||||
clip_rect: None,
|
||||
pointer_events: element.style.pointer_events,
|
||||
focusable: element.style.focusable,
|
||||
cursor: element.style.cursor.unwrap_or(CursorIcon::Default),
|
||||
scroll_metrics: None,
|
||||
prepared_image: None,
|
||||
prepared_text: None,
|
||||
children: Vec::new(),
|
||||
};
|
||||
}
|
||||
if let Some(clip) = clip_rect
|
||||
&& !rects_overlap(rect, clip)
|
||||
{
|
||||
perf_stats.viewport_culled += 1;
|
||||
return LayoutNode {
|
||||
path,
|
||||
element_id: element.id,
|
||||
rect,
|
||||
corner_radius: 0.0,
|
||||
clip_rect: None,
|
||||
pointer_events: element.style.pointer_events,
|
||||
focusable: element.style.focusable,
|
||||
cursor: element.style.cursor.unwrap_or(CursorIcon::Default),
|
||||
scroll_metrics: None,
|
||||
prepared_image: None,
|
||||
prepared_text: None,
|
||||
children: Vec::new(),
|
||||
};
|
||||
}
|
||||
|
||||
// Incremental layout cache check.
|
||||
@@ -368,7 +378,13 @@ fn layout_element(
|
||||
if pushed_clip {
|
||||
scene.pop_clip();
|
||||
}
|
||||
cache_layout(layout_cache, cache_key, &interaction, &scene.items[scene_start..], rect.origin);
|
||||
cache_layout(
|
||||
layout_cache,
|
||||
cache_key,
|
||||
&interaction,
|
||||
&scene.items[scene_start..],
|
||||
rect.origin,
|
||||
);
|
||||
return interaction;
|
||||
}
|
||||
|
||||
@@ -382,7 +398,13 @@ fn layout_element(
|
||||
if pushed_clip {
|
||||
scene.pop_clip();
|
||||
}
|
||||
cache_layout(layout_cache, cache_key, &interaction, &scene.items[scene_start..], rect.origin);
|
||||
cache_layout(
|
||||
layout_cache,
|
||||
cache_key,
|
||||
&interaction,
|
||||
&scene.items[scene_start..],
|
||||
rect.origin,
|
||||
);
|
||||
return interaction;
|
||||
}
|
||||
|
||||
@@ -508,7 +530,13 @@ fn layout_element(
|
||||
if pushed_clip {
|
||||
scene.pop_clip();
|
||||
}
|
||||
cache_layout(layout_cache, cache_key, &interaction, &scene.items[scene_start..], rect.origin);
|
||||
cache_layout(
|
||||
layout_cache,
|
||||
cache_key,
|
||||
&interaction,
|
||||
&scene.items[scene_start..],
|
||||
rect.origin,
|
||||
);
|
||||
return interaction;
|
||||
}
|
||||
|
||||
@@ -518,7 +546,13 @@ fn layout_element(
|
||||
if pushed_clip {
|
||||
scene.pop_clip();
|
||||
}
|
||||
cache_layout(layout_cache, cache_key, &interaction, &scene.items[scene_start..], rect.origin);
|
||||
cache_layout(
|
||||
layout_cache,
|
||||
cache_key,
|
||||
&interaction,
|
||||
&scene.items[scene_start..],
|
||||
rect.origin,
|
||||
);
|
||||
return interaction;
|
||||
}
|
||||
|
||||
@@ -527,7 +561,13 @@ fn layout_element(
|
||||
if pushed_clip {
|
||||
scene.pop_clip();
|
||||
}
|
||||
cache_layout(layout_cache, cache_key, &interaction, &scene.items[scene_start..], rect.origin);
|
||||
cache_layout(
|
||||
layout_cache,
|
||||
cache_key,
|
||||
&interaction,
|
||||
&scene.items[scene_start..],
|
||||
rect.origin,
|
||||
);
|
||||
return interaction;
|
||||
}
|
||||
interaction.children = layout_container_children(
|
||||
@@ -545,7 +585,13 @@ fn layout_element(
|
||||
scene.pop_clip();
|
||||
}
|
||||
|
||||
cache_layout(layout_cache, cache_key, &interaction, &scene.items[scene_start..], rect.origin);
|
||||
cache_layout(
|
||||
layout_cache,
|
||||
cache_key,
|
||||
&interaction,
|
||||
&scene.items[scene_start..],
|
||||
rect.origin,
|
||||
);
|
||||
interaction
|
||||
}
|
||||
|
||||
@@ -558,10 +604,13 @@ fn cache_layout(
|
||||
origin: Point,
|
||||
) {
|
||||
let neg = Point::new(-origin.x, -origin.y);
|
||||
cache.results.insert(key, CachedLayout {
|
||||
interaction_node: interaction.clone().translated(neg),
|
||||
scene_items: scene_items.iter().map(|i| i.translated(neg)).collect(),
|
||||
});
|
||||
cache.results.insert(
|
||||
key,
|
||||
CachedLayout {
|
||||
interaction_node: interaction.clone().translated(neg),
|
||||
scene_items: scene_items.iter().map(|i| i.translated(neg)).collect(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn hit_path_node(node: &LayoutNode, point: crate::scene::Point) -> Option<Vec<HitTarget>> {
|
||||
@@ -735,6 +784,7 @@ struct MeasuredChild {
|
||||
is_flex: bool,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_container_children(
|
||||
element: &Element,
|
||||
content: Rect,
|
||||
@@ -986,7 +1036,14 @@ fn intrinsic_size(
|
||||
return cached;
|
||||
}
|
||||
let insets = content_insets(&element.style);
|
||||
let result = intrinsic_size_inner(element, available_size, insets, text_system, perf_stats, layout_cache);
|
||||
let result = intrinsic_size_inner(
|
||||
element,
|
||||
available_size,
|
||||
insets,
|
||||
text_system,
|
||||
perf_stats,
|
||||
layout_cache,
|
||||
);
|
||||
layout_cache.intrinsic_cache.insert(cache_key, result);
|
||||
result
|
||||
}
|
||||
@@ -1047,8 +1104,13 @@ fn intrinsic_size_inner(
|
||||
);
|
||||
}
|
||||
|
||||
let intrinsic_content =
|
||||
intrinsic_container_content_size(element, content_size, text_system, perf_stats, layout_cache);
|
||||
let intrinsic_content = intrinsic_container_content_size(
|
||||
element,
|
||||
content_size,
|
||||
text_system,
|
||||
perf_stats,
|
||||
layout_cache,
|
||||
);
|
||||
|
||||
UiSize::new(
|
||||
explicit_width
|
||||
@@ -1539,9 +1601,9 @@ fn child_rect(
|
||||
mod tests {
|
||||
use super::{LayoutCache, layout_scene, layout_snapshot, layout_snapshot_with_cache};
|
||||
use crate::scene::{Color, DisplayItem, Point, Quad, Rect, UiSize};
|
||||
use crate::text::TextSystem;
|
||||
use crate::text::{TextStyle, TextWrap};
|
||||
use crate::tree::{Edges, Element, ElementId, FlexDirection};
|
||||
use crate::text::TextSystem;
|
||||
|
||||
#[test]
|
||||
fn row_layout_apportions_fixed_and_flex_children() {
|
||||
@@ -2156,10 +2218,12 @@ mod tests {
|
||||
let size = UiSize::new(400.0, 600.0);
|
||||
let snap1 = layout_snapshot_with_cache(1, size, &root, &mut text_system, &mut layout_cache);
|
||||
let snap2 = layout_snapshot_with_cache(2, size, &root, &mut text_system, &mut layout_cache);
|
||||
assert_eq!(snap1.scene.items, snap2.scene.items, "scene items should be identical");
|
||||
assert_eq!(
|
||||
snap1.interaction_tree.root,
|
||||
snap2.interaction_tree.root,
|
||||
snap1.scene.items, snap2.scene.items,
|
||||
"scene items should be identical"
|
||||
);
|
||||
assert_eq!(
|
||||
snap1.interaction_tree.root, snap2.interaction_tree.root,
|
||||
"interaction trees should be identical"
|
||||
);
|
||||
}
|
||||
@@ -2198,12 +2262,14 @@ mod tests {
|
||||
let item_height = 40.0;
|
||||
let viewport_height = 200.0;
|
||||
let n = 100;
|
||||
let mut scroll_box = Element::scroll_box(0.0).width(400.0).height(viewport_height);
|
||||
let mut scroll_box = Element::scroll_box(0.0)
|
||||
.width(400.0)
|
||||
.height(viewport_height);
|
||||
for i in 0..n {
|
||||
scroll_box = scroll_box.child(
|
||||
Element::new()
|
||||
.height(item_height)
|
||||
.child(Element::text(format!("item {i}"), text_style()))
|
||||
.child(Element::text(format!("item {i}"), text_style())),
|
||||
);
|
||||
}
|
||||
let root = Element::new().child(scroll_box);
|
||||
@@ -2214,11 +2280,20 @@ mod tests {
|
||||
let snap = layout_snapshot_with_cache(1, size, &root, &mut text_system, &mut layout_cache);
|
||||
|
||||
// Only text items within the 200px viewport should appear in the scene.
|
||||
let text_items = snap.scene.items.iter().filter(|item| {
|
||||
matches!(item, DisplayItem::Text(_))
|
||||
}).count();
|
||||
let text_items = snap
|
||||
.scene
|
||||
.items
|
||||
.iter()
|
||||
.filter(|item| matches!(item, DisplayItem::Text(_)))
|
||||
.count();
|
||||
// At most 6 text items should be visible (5 fit + 1 partial).
|
||||
assert!(text_items <= 6, "expected <= 6 text items in scene, got {text_items} (culling not working)");
|
||||
assert!(text_items >= 4, "expected >= 4 text items visible, got {text_items}");
|
||||
assert!(
|
||||
text_items <= 6,
|
||||
"expected <= 6 text items in scene, got {text_items} (culling not working)"
|
||||
);
|
||||
assert!(
|
||||
text_items >= 4,
|
||||
"expected >= 4 text items visible, got {text_items}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
pub(crate) mod trace_targets {
|
||||
pub const PLATFORM: &str = "ruin_ui::platform";
|
||||
pub const SCENE: &str = "ruin_ui::scene";
|
||||
#[allow(dead_code)]
|
||||
pub const TEXT_PERF: &str = "ruin_ui::text_perf";
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ pub struct PlatformProxy {
|
||||
pub struct PlatformRuntime {
|
||||
proxy: PlatformProxy,
|
||||
events: mpsc::Receiver<PlatformEvent>,
|
||||
_worker: WorkerHandle,
|
||||
worker: Option<WorkerHandle>,
|
||||
}
|
||||
|
||||
pub struct PlatformEndpoint {
|
||||
@@ -73,6 +73,7 @@ pub enum PlatformEvent {
|
||||
window_id: WindowId,
|
||||
text: String,
|
||||
},
|
||||
#[cfg(target_os = "linux")]
|
||||
PrimarySelectionText {
|
||||
window_id: WindowId,
|
||||
text: String,
|
||||
@@ -118,10 +119,12 @@ pub enum PlatformRequest {
|
||||
RequestClipboardText {
|
||||
window_id: WindowId,
|
||||
},
|
||||
#[cfg(target_os = "linux")]
|
||||
SetPrimarySelectionText {
|
||||
window_id: WindowId,
|
||||
text: String,
|
||||
},
|
||||
#[cfg(target_os = "linux")]
|
||||
RequestPrimarySelectionText {
|
||||
window_id: WindowId,
|
||||
},
|
||||
@@ -203,7 +206,30 @@ impl PlatformRuntime {
|
||||
Self {
|
||||
proxy,
|
||||
events: event_rx,
|
||||
_worker: worker,
|
||||
worker: Some(worker),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a platform runtime hosted by the current runtime thread.
|
||||
///
|
||||
/// This is useful for backends that must run on a specific host thread
|
||||
/// (for example, AppKit on macOS main thread).
|
||||
pub fn custom_local(start: impl FnOnce(PlatformEndpoint)) -> Self {
|
||||
let (command_tx, command_rx) = mpsc::unbounded_channel::<PlatformRequest>();
|
||||
let (event_tx, event_rx) = mpsc::unbounded_channel::<PlatformEvent>();
|
||||
let proxy = PlatformProxy {
|
||||
command_tx,
|
||||
next_window_id: Arc::new(AtomicU64::new(1)),
|
||||
};
|
||||
start(PlatformEndpoint {
|
||||
commands: command_rx,
|
||||
events: event_tx,
|
||||
});
|
||||
|
||||
Self {
|
||||
proxy,
|
||||
events: event_rx,
|
||||
worker: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,6 +242,7 @@ impl PlatformRuntime {
|
||||
}
|
||||
|
||||
pub fn take_pending_events(&mut self) -> Vec<PlatformEvent> {
|
||||
let _ = self.worker.as_ref();
|
||||
let mut events = Vec::new();
|
||||
while let Ok(event) = self.events.try_recv() {
|
||||
events.push(event);
|
||||
@@ -247,6 +274,7 @@ impl PlatformProxy {
|
||||
self.send(PlatformRequest::ReplaceScene { window_id, scene })
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn set_primary_selection_text(
|
||||
&self,
|
||||
window_id: WindowId,
|
||||
@@ -258,6 +286,7 @@ impl PlatformProxy {
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn request_primary_selection_text(
|
||||
&self,
|
||||
window_id: WindowId,
|
||||
@@ -356,7 +385,9 @@ pub fn start_headless() -> PlatformRuntime {
|
||||
}
|
||||
PlatformRequest::SetClipboardText { .. } => {}
|
||||
PlatformRequest::RequestClipboardText { .. } => {}
|
||||
#[cfg(target_os = "linux")]
|
||||
PlatformRequest::SetPrimarySelectionText { .. } => {}
|
||||
#[cfg(target_os = "linux")]
|
||||
PlatformRequest::RequestPrimarySelectionText { .. } => {}
|
||||
PlatformRequest::SetCursorIcon { .. } => {}
|
||||
PlatformRequest::EmitCloseRequested { window_id } => {
|
||||
|
||||
@@ -110,6 +110,7 @@ impl WindowController {
|
||||
}
|
||||
|
||||
/// Copies plain text to the platform primary-selection buffer for this window.
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn set_primary_selection_text(
|
||||
&self,
|
||||
text: impl Into<String>,
|
||||
@@ -128,6 +129,7 @@ impl WindowController {
|
||||
}
|
||||
|
||||
/// Requests the current plain-text primary selection contents from the platform.
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn request_primary_selection_text(&self) -> Result<(), PlatformClosed> {
|
||||
self.proxy.request_primary_selection_text(self.id)
|
||||
}
|
||||
|
||||
@@ -219,6 +219,7 @@ impl Deref for PreparedText {
|
||||
|
||||
impl PreparedText {
|
||||
/// Construct a `PreparedText` from shaped data. Called by `TextSystem::prepare_spans`.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn from_layout(
|
||||
element_id: Option<ElementId>,
|
||||
text: String,
|
||||
@@ -623,10 +624,18 @@ impl DisplayItem {
|
||||
/// Return a copy of this item with all positions shifted by `offset`.
|
||||
pub fn translated(&self, offset: Point) -> Self {
|
||||
fn translate_rect(r: Rect, o: Point) -> Rect {
|
||||
Rect::new(r.origin.x + o.x, r.origin.y + o.y, r.size.width, r.size.height)
|
||||
Rect::new(
|
||||
r.origin.x + o.x,
|
||||
r.origin.y + o.y,
|
||||
r.size.width,
|
||||
r.size.height,
|
||||
)
|
||||
}
|
||||
match self {
|
||||
Self::Quad(q) => Self::Quad(Quad { rect: translate_rect(q.rect, offset), ..*q }),
|
||||
Self::Quad(q) => Self::Quad(Quad {
|
||||
rect: translate_rect(q.rect, offset),
|
||||
..*q
|
||||
}),
|
||||
Self::RoundedRect(r) => Self::RoundedRect(RoundedRect {
|
||||
rect: translate_rect(r.rect, offset),
|
||||
..*r
|
||||
@@ -838,8 +847,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn prepared_text_vertical_offset_moves_between_lines() {
|
||||
use std::sync::Arc;
|
||||
use super::{PreparedTextLine, TextLayoutData};
|
||||
use std::sync::Arc;
|
||||
|
||||
let mut text = PreparedText::monospace(
|
||||
"abcdwxyz",
|
||||
@@ -873,7 +882,11 @@ mod tests {
|
||||
}
|
||||
}
|
||||
let orig_size = text.layout.size;
|
||||
text.layout = Arc::new(TextLayoutData { lines, glyphs, size: orig_size });
|
||||
text.layout = Arc::new(TextLayoutData {
|
||||
lines,
|
||||
glyphs,
|
||||
size: orig_size,
|
||||
});
|
||||
|
||||
assert_eq!(text.vertical_offset(2, 1), Some(6));
|
||||
assert_eq!(text.vertical_offset(6, -1), Some(2));
|
||||
@@ -892,8 +905,12 @@ mod tests {
|
||||
let target_line = &text.lines[1];
|
||||
// Lines store LOCAL coords; add text.origin to get absolute window coords for the query.
|
||||
let y = text.origin.y + target_line.rect.origin.y + target_line.rect.size.height * 0.5;
|
||||
let start = text.byte_offset_for_position(Point::new(text.origin.x + target_line.rect.origin.x, y));
|
||||
let end = text.byte_offset_for_position(Point::new(text.origin.x + target_line.rect.origin.x + 16.0, y));
|
||||
let start =
|
||||
text.byte_offset_for_position(Point::new(text.origin.x + target_line.rect.origin.x, y));
|
||||
let end = text.byte_offset_for_position(Point::new(
|
||||
text.origin.x + target_line.rect.origin.x + 16.0,
|
||||
y,
|
||||
));
|
||||
let rects = text.selection_rects(start, end);
|
||||
|
||||
assert_eq!(rects.len(), 1);
|
||||
|
||||
@@ -10,8 +10,9 @@ use cosmic_text::{
|
||||
};
|
||||
use fontconfig::Fontconfig;
|
||||
|
||||
use crate::{Color, GlyphInstance, Point, PreparedText, PreparedTextLine, Rect, TextLayoutData,
|
||||
UiSize};
|
||||
use crate::{
|
||||
Color, GlyphInstance, Point, PreparedText, PreparedTextLine, Rect, TextLayoutData, UiSize,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum TextAlign {
|
||||
@@ -443,7 +444,11 @@ impl TextSystem {
|
||||
// For no-wrap + start-aligned text, width doesn't affect shaping.
|
||||
// Pass None so cosmic-text doesn't truncate and the result is
|
||||
// consistent with the width-independent cache key.
|
||||
let effective_width = if width_affects_layout(style) { width } else { None };
|
||||
let effective_width = if width_affects_layout(style) {
|
||||
width
|
||||
} else {
|
||||
None
|
||||
};
|
||||
borrowed.set_size(effective_width, None);
|
||||
let default_attrs = default_attrs_for_style(style, default_family.as_deref());
|
||||
if uses_plain_text_fast_path {
|
||||
@@ -495,9 +500,7 @@ impl TextSystem {
|
||||
// is color-independent. Per-span colors (from glyph.color_opt)
|
||||
// are stored directly since they are part of the shape key via
|
||||
// the spans hash.
|
||||
color: glyph
|
||||
.color_opt
|
||||
.map_or(Color::SENTINEL, color_from_cosmic),
|
||||
color: glyph.color_opt.map_or(Color::SENTINEL, color_from_cosmic),
|
||||
cache_key: Some(physical.cache_key),
|
||||
text_start: line_offset + glyph.start,
|
||||
text_end: line_offset + glyph.end,
|
||||
@@ -696,16 +699,16 @@ fn attrs_for_span<'a>(
|
||||
base
|
||||
};
|
||||
base.weight(match span.weight {
|
||||
TextSpanWeight::Normal => CosmicWeight::NORMAL,
|
||||
TextSpanWeight::Medium => CosmicWeight::MEDIUM,
|
||||
TextSpanWeight::Semibold => CosmicWeight::SEMIBOLD,
|
||||
TextSpanWeight::Bold => CosmicWeight::BOLD,
|
||||
})
|
||||
.style(match span.slant {
|
||||
TextSpanSlant::Normal => CosmicStyle::Normal,
|
||||
TextSpanSlant::Italic => CosmicStyle::Italic,
|
||||
TextSpanSlant::Oblique => CosmicStyle::Oblique,
|
||||
})
|
||||
TextSpanWeight::Normal => CosmicWeight::NORMAL,
|
||||
TextSpanWeight::Medium => CosmicWeight::MEDIUM,
|
||||
TextSpanWeight::Semibold => CosmicWeight::SEMIBOLD,
|
||||
TextSpanWeight::Bold => CosmicWeight::BOLD,
|
||||
})
|
||||
.style(match span.slant {
|
||||
TextSpanSlant::Normal => CosmicStyle::Normal,
|
||||
TextSpanSlant::Italic => CosmicStyle::Italic,
|
||||
TextSpanSlant::Oblique => CosmicStyle::Oblique,
|
||||
})
|
||||
}
|
||||
|
||||
fn to_cosmic_color(color: Color) -> CosmicColor {
|
||||
@@ -930,7 +933,9 @@ impl std::hash::Hash for TextStyle {
|
||||
self.line_height.to_bits().hash(state);
|
||||
self.color.hash(state);
|
||||
self.font_family.hash(state);
|
||||
self.bounds.map(|b| (b.width.to_bits(), b.height.to_bits())).hash(state);
|
||||
self.bounds
|
||||
.map(|b| (b.width.to_bits(), b.height.to_bits()))
|
||||
.hash(state);
|
||||
self.wrap.hash(state);
|
||||
self.align.hash(state);
|
||||
self.max_lines.hash(state);
|
||||
@@ -1038,16 +1043,32 @@ mod tests {
|
||||
let blue = text_system.prepare("hello", origin, &style_blue);
|
||||
// Both PreparedTexts must have identical glyph shapes and sentinel colors —
|
||||
// the second call hits the shape cache and produces the same layout data.
|
||||
assert_eq!(red.glyphs.len(), blue.glyphs.len(), "glyph count must match");
|
||||
assert_eq!(
|
||||
red.glyphs.len(),
|
||||
blue.glyphs.len(),
|
||||
"glyph count must match"
|
||||
);
|
||||
for (r, b) in red.glyphs.iter().zip(blue.glyphs.iter()) {
|
||||
assert_eq!(r.position, b.position, "glyph positions must match");
|
||||
assert_eq!(r.cache_key, b.cache_key, "glyph cache keys must match");
|
||||
assert_eq!(r.color, Color::SENTINEL, "default-color glyphs must use SENTINEL");
|
||||
assert_eq!(b.color, Color::SENTINEL, "default-color glyphs must use SENTINEL");
|
||||
assert_eq!(
|
||||
r.color,
|
||||
Color::SENTINEL,
|
||||
"default-color glyphs must use SENTINEL"
|
||||
);
|
||||
assert_eq!(
|
||||
b.color,
|
||||
Color::SENTINEL,
|
||||
"default-color glyphs must use SENTINEL"
|
||||
);
|
||||
}
|
||||
// PreparedText::clone() is an Arc clone — O(1).
|
||||
let cloned = red.clone();
|
||||
assert_eq!(cloned.layout_ptr(), red.layout_ptr(), "clone must share the same Arc");
|
||||
assert_eq!(
|
||||
cloned.layout_ptr(),
|
||||
red.layout_ptr(),
|
||||
"clone must share the same Arc"
|
||||
);
|
||||
// But they should carry different default colors.
|
||||
assert_eq!(red.default_color, Color::rgb(0xFF, 0x00, 0x00));
|
||||
assert_eq!(blue.default_color, Color::rgb(0x00, 0x00, 0xFF));
|
||||
|
||||
Reference in New Issue
Block a user