#![allow(non_snake_case)]
use crate::composable;
use crate::modifier::{GraphicsLayer, Modifier, TransformOrigin};
use crate::round_scaling_list::{
leading_auto_centring_spacer, place_row_with, round_to_px, trailing_auto_centring_spacer,
CentreAnchor, PlacedRow, ScaleAlpha, ScalingParams,
};
use crate::round_scroll_indicator::{
scaling_list_items_with, IndicatorItem, ScalingList, ThumbLength,
};
use crate::subcompose_layout::{
MeasurePolicy as SubcomposeMeasurePolicy, SubcomposeChild, SubcomposeLayoutNode,
SubcomposeMeasureScope, SubcomposeMeasureScopeImpl,
};
use crate::widgets::wear::density::WearDensity;
use crate::widgets::Layout;
use cranpose_core::{remember, useState, MutableState, NodeId, SlotId};
use cranpose_ui_graphics::{CompositingStrategy, Size};
use cranpose_ui_layout::{Constraints, Measurable, MeasurePolicy, MeasureResult, Placement};
use std::cell::{Cell, RefCell};
use std::rc::Rc;
#[derive(Clone, Debug)]
pub struct WearItemTransform {
cell: Rc<Cell<ScaleAlpha>>,
}
impl WearItemTransform {
pub fn new() -> Self {
Self {
cell: Rc::new(Cell::new(ScaleAlpha::UNCHANGED)),
}
}
pub fn get(&self) -> ScaleAlpha {
self.cell.get()
}
pub fn set(&self, value: ScaleAlpha) {
self.cell.set(value);
}
}
impl Default for WearItemTransform {
fn default() -> Self {
Self::new()
}
}
impl PartialEq for WearItemTransform {
fn eq(&self, other: &Self) -> bool {
Rc::ptr_eq(&self.cell, &other.cell)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub struct WearScalingLayoutInfo {
pub item_count: usize,
pub viewport: f32,
pub first_centre: f32,
pub last_centre: f32,
pub content: f32,
pub visible: usize,
pub composed: usize,
}
impl WearScalingLayoutInfo {
pub fn travel(self) -> f32 {
(self.last_centre - self.first_centre).max(0.0)
}
pub fn scrolled(self) -> f32 {
(self.viewport * 0.5 - self.first_centre).clamp(0.0, self.travel())
}
}
#[derive(Clone)]
pub struct WearScalingListState {
anchor: MutableState<CentreAnchor>,
layout: Rc<RefCell<WearScalingLayoutInfo>>,
heights: Rc<RefCell<ItemHeights>>,
indicator: Rc<RefCell<IndicatorState>>,
}
impl PartialEq for WearScalingListState {
fn eq(&self, other: &Self) -> bool {
self.anchor == other.anchor
&& Rc::ptr_eq(&self.layout, &other.layout)
&& Rc::ptr_eq(&self.heights, &other.heights)
&& Rc::ptr_eq(&self.indicator, &other.indicator)
}
}
#[derive(Debug, Default)]
struct IndicatorState {
visible: Vec<IndicatorItem>,
total: usize,
viewport: f32,
before_padding: f32,
after_padding: f32,
thumb: ThumbLength,
}
impl IndicatorState {
fn clear(&mut self) {
self.visible.clear();
self.total = 0;
self.viewport = 0.0;
self.before_padding = 0.0;
self.after_padding = 0.0;
}
fn record(
&mut self,
window: &[WindowedRow],
spec: &WearScalingLazyColumnSpec,
known: &ItemHeights,
viewport: f32,
density: WearDensity,
) {
let count = known.len();
if count == 0 {
self.clear();
return;
}
let viewport_px = density.to_px(viewport).round();
self.total = count;
self.viewport = viewport_px;
scaling_list_items_with(
spec.scaling,
viewport,
density.density(),
window.iter().map(|row| (row.top, row.height)),
&mut self.visible,
);
if let Some(base) = window.first().map(|row| row.index) {
for item in &mut self.visible {
item.index += base;
}
}
let mut before = density.to_px(density.dp(spec.content_padding_top));
let mut after = density.to_px(density.dp(spec.content_padding_bottom));
if let Some(anchor) = spec.auto_centering {
let index = anchor.index.min(count - 1);
let spacing = density.dp(spec.item_spacing);
let centre = (0..index)
.map(|i| known.height_of(i) + spacing)
.sum::<f32>()
+ known.height_of(index) * 0.5;
before += leading_auto_centring_spacer(
viewport_px,
density.to_px(centre).round(),
density.to_px(anchor.offset),
);
after += trailing_auto_centring_spacer(
viewport_px,
density.to_px(known.height_of(count - 1)).round(),
);
}
self.before_padding = before;
self.after_padding = after;
}
}
#[derive(Default, Debug)]
struct ItemHeights {
known: Vec<Option<f32>>,
sum: f32,
count: usize,
}
impl ItemHeights {
fn len(&self) -> usize {
self.known.len()
}
fn resize(&mut self, len: usize) {
if self.known.len() == len {
return;
}
self.known.clear();
self.known.resize(len, None);
self.sum = 0.0;
self.count = 0;
}
fn record(&mut self, index: usize, height: f32) {
let Some(slot) = self.known.get_mut(index) else {
return;
};
match slot.replace(height) {
Some(previous) => self.sum += height - previous,
None => {
self.sum += height;
self.count += 1;
}
}
}
fn estimate(&self) -> f32 {
if self.count == 0 {
0.0
} else {
self.sum / self.count as f32
}
}
fn height_of(&self, index: usize) -> f32 {
self.known
.get(index)
.copied()
.flatten()
.unwrap_or_else(|| self.estimate())
}
}
impl WearScalingListState {
pub fn anchor(&self) -> CentreAnchor {
self.anchor.get()
}
pub fn set_anchor(&self, anchor: CentreAnchor) {
self.anchor.set(anchor);
}
pub fn scroll_by(&self, delta: f32) {
if !delta.is_finite() {
return;
}
let info = *self.layout.borrow();
let anchor = self.anchor.get();
self.set_anchor(re_anchor(anchor, delta, info));
}
pub fn layout_info(&self) -> WearScalingLayoutInfo {
*self.layout.borrow()
}
pub fn with_indicator_list<R>(
&self,
read: impl FnOnce(&mut ThumbLength, ScalingList<'_>) -> R,
) -> R {
let mut indicator = self.indicator.borrow_mut();
let IndicatorState {
visible,
total,
viewport,
before_padding,
after_padding,
thumb,
} = &mut *indicator;
read(
thumb,
ScalingList {
visible: visible.as_slice(),
total: *total,
viewport: *viewport,
before_padding: *before_padding,
after_padding: *after_padding,
},
)
}
}
fn re_anchor(anchor: CentreAnchor, delta: f32, info: WearScalingLayoutInfo) -> CentreAnchor {
let mut anchor = CentreAnchor {
index: anchor.index,
offset: anchor.offset + delta,
};
if info.item_count == 0 {
return anchor;
}
let travelled = info.scrolled() + delta;
let travel = info.travel();
if travelled < 0.0 {
anchor.offset -= travelled;
} else if travelled > travel {
anchor.offset -= travelled - travel;
}
anchor
}
#[composable]
pub fn rememberWearScalingListState(initial: CentreAnchor) -> WearScalingListState {
let anchor = useState(move || initial);
let layout = remember(|| Rc::new(RefCell::new(WearScalingLayoutInfo::default())))
.with(|value| value.clone());
let heights =
remember(|| Rc::new(RefCell::new(ItemHeights::default()))).with(|value| value.clone());
let indicator =
remember(|| Rc::new(RefCell::new(IndicatorState::default()))).with(|value| value.clone());
WearScalingListState {
anchor,
layout,
heights,
indicator,
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct WearScalingLazyColumnSpec {
pub scaling: ScalingParams,
pub item_spacing: f32,
pub content_padding_start: f32,
pub content_padding_end: f32,
pub content_padding_top: f32,
pub content_padding_bottom: f32,
pub auto_centering: Option<CentreAnchor>,
pub compositing_strategy: CompositingStrategy,
pub beyond_bounds_item_count: usize,
}
impl Default for WearScalingLazyColumnSpec {
fn default() -> Self {
Self {
scaling: ScalingParams::WEAR,
item_spacing: 4.0,
content_padding_start: 0.0,
content_padding_end: 0.0,
content_padding_top: 0.0,
content_padding_bottom: 0.0,
auto_centering: Some(CentreAnchor::default()),
compositing_strategy: CompositingStrategy::Auto,
beyond_bounds_item_count: 2,
}
}
}
impl WearScalingLazyColumnSpec {
pub fn content_padding(mut self, horizontal: f32, vertical: f32) -> Self {
self.content_padding_start = horizontal;
self.content_padding_end = horizontal;
self.content_padding_top = vertical;
self.content_padding_bottom = vertical;
self
}
pub fn item_spacing(mut self, spacing: f32) -> Self {
self.item_spacing = spacing;
self
}
pub fn scaling(mut self, scaling: ScalingParams) -> Self {
self.scaling = scaling;
self
}
pub fn compositing_strategy(mut self, strategy: CompositingStrategy) -> Self {
self.compositing_strategy = strategy;
self
}
pub fn auto_centering(mut self, anchor: Option<CentreAnchor>) -> Self {
self.auto_centering = anchor;
self
}
pub fn beyond_bounds_item_count(mut self, count: usize) -> Self {
self.beyond_bounds_item_count = count;
self
}
}
#[derive(Default)]
pub struct WearScalingListScope {
items: Vec<Rc<dyn Fn()>>,
}
impl WearScalingListScope {
pub fn item<F>(&mut self, content: F)
where
F: Fn() + 'static,
{
self.items.push(Rc::new(content));
}
pub fn items<F>(&mut self, count: usize, item: F)
where
F: Fn(usize) + Clone + 'static,
{
for index in 0..count {
let item = item.clone();
self.item(move || item(index));
}
}
pub fn count(&self) -> usize {
self.items.len()
}
}
#[derive(Clone)]
pub struct WearScalingListContent {
items: Rc<Vec<Rc<dyn Fn()>>>,
}
impl PartialEq for WearScalingListContent {
fn eq(&self, other: &Self) -> bool {
Rc::ptr_eq(&self.items, &other.items)
}
}
impl Default for WearScalingListContent {
fn default() -> Self {
Self {
items: Rc::new(Vec::new()),
}
}
}
#[composable]
pub fn WearScalingItem<F>(
modifier: Modifier,
transform: WearItemTransform,
compositing_strategy: CompositingStrategy,
content: F,
) -> NodeId
where
F: FnMut() + 'static,
{
let layer_transform = transform.clone();
let layered = modifier.graphics_layer(move || {
let value = layer_transform.get();
GraphicsLayer {
alpha: value.alpha,
scale: value.scale,
transform_origin: TransformOrigin::new(0.5, 0.0),
compositing_strategy,
..GraphicsLayer::default()
}
});
Layout(layered, WearItemMeasurePolicy, content)
}
#[derive(Clone, Debug, PartialEq)]
struct WearItemMeasurePolicy;
impl MeasurePolicy for WearItemMeasurePolicy {
fn measure(
&self,
measurables: &[Box<dyn Measurable>],
constraints: Constraints,
) -> MeasureResult {
let mut placements = Vec::new();
let size = self.measure_into(measurables, constraints, &mut placements);
MeasureResult::new(size, placements)
}
fn measure_into(
&self,
measurables: &[Box<dyn Measurable>],
constraints: Constraints,
placements: &mut Vec<Placement>,
) -> Size {
placements.clear();
let mut width = constraints.min_width;
let mut height = constraints.min_height;
for measurable in measurables {
let placeable = measurable.measure(constraints);
width = width.max(placeable.width());
height = height.max(placeable.height());
placements.push(Placement::new(placeable.node_id(), 0.0, 0.0, 0));
}
Size::new(
width.clamp(constraints.min_width, constraints.max_width),
height.clamp(constraints.min_height, constraints.max_height),
)
}
fn min_intrinsic_width(&self, measurables: &[Box<dyn Measurable>], height: f32) -> f32 {
measurables
.iter()
.map(|m| m.min_intrinsic_width(height))
.fold(0.0, f32::max)
}
fn max_intrinsic_width(&self, measurables: &[Box<dyn Measurable>], height: f32) -> f32 {
measurables
.iter()
.map(|m| m.max_intrinsic_width(height))
.fold(0.0, f32::max)
}
fn min_intrinsic_height(&self, measurables: &[Box<dyn Measurable>], width: f32) -> f32 {
measurables
.iter()
.map(|m| m.min_intrinsic_height(width))
.fold(0.0, f32::max)
}
fn max_intrinsic_height(&self, measurables: &[Box<dyn Measurable>], width: f32) -> f32 {
measurables
.iter()
.map(|m| m.max_intrinsic_height(width))
.fold(0.0, f32::max)
}
}
pub fn WearScalingLazyColumn<F>(
modifier: Modifier,
state: WearScalingListState,
spec: WearScalingLazyColumnSpec,
content: F,
) -> NodeId
where
F: FnOnce(&mut WearScalingListScope),
{
let mut scope = WearScalingListScope::default();
content(&mut scope);
WearScalingLazyColumnNode(
modifier,
state,
spec,
WearScalingListContent {
items: Rc::new(scope.items),
},
)
}
#[derive(Default)]
struct WearScalingListInputs {
spec: WearScalingLazyColumnSpec,
anchor: CentreAnchor,
content: WearScalingListContent,
}
#[composable]
pub fn WearScalingLazyColumnNode(
modifier: Modifier,
state: WearScalingListState,
spec: WearScalingLazyColumnSpec,
content: WearScalingListContent,
) -> NodeId {
let transforms = remember(|| Rc::new(RefCell::new(Vec::<WearItemTransform>::new())))
.with(|value| value.clone());
let inputs = remember(|| Rc::new(RefCell::new(WearScalingListInputs::default())))
.with(|value| value.clone());
let anchor = if spec.auto_centering.is_some() {
state.anchor()
} else {
CentreAnchor {
index: 0,
offset: 0.0,
}
};
transforms
.borrow_mut()
.resize_with(content.items.len(), WearItemTransform::new);
let inputs_changed = {
let mut current = inputs.borrow_mut();
let changed =
current.spec != spec || current.anchor != anchor || current.content != content;
if changed {
current.spec = spec;
current.anchor = anchor;
current.content = content;
}
changed
};
let policy: Rc<SubcomposeMeasurePolicy> = remember({
let inputs = inputs.clone();
let transforms = transforms.clone();
let layout = state.layout.clone();
let heights = state.heights.clone();
let indicator = state.indicator.clone();
move || {
let policy: Rc<SubcomposeMeasurePolicy> = Rc::new(
move |scope: &mut SubcomposeMeasureScopeImpl<'_>, constraints: Constraints| {
measure_wear_scaling_list(
scope,
constraints,
&inputs.borrow(),
&transforms,
&layout,
&heights,
&indicator,
)
},
);
policy
}
})
.with(|policy| policy.clone());
let modifier = modifier.clip_to_bounds();
let list_id = Rc::as_ptr(&state.layout) as usize;
let node_id = cranpose_core::with_current_composer(|composer| {
composer.with_key(&(list_id, "WearScalingLazyColumnNode"), |composer| {
composer.emit_node({
let modifier = modifier.clone();
let policy = Rc::clone(&policy);
move || SubcomposeLayoutNode::with_content_type_policy(modifier, policy)
})
})
});
let captured_context =
cranpose_core::with_current_composer(|composer| composer.capture_composition_context());
if let Err(err) = cranpose_core::with_node_mut(node_id, |node: &mut SubcomposeLayoutNode| {
if !node.modifier().structural_eq(&modifier) {
node.set_modifier(modifier.clone());
}
node.set_measure_policy(Rc::clone(&policy));
node.set_captured_context(captured_context);
if inputs_changed {
node.request_measure_recompose();
}
}) {
debug_assert!(false, "failed to update WearScalingLazyColumn node: {err}");
}
node_id
}
struct WindowedRow {
index: usize,
roots: Vec<(NodeId, f32, f32)>,
top: f32,
height: f32,
placed: PlacedRow,
}
fn measure_wear_scaling_list(
scope: &mut SubcomposeMeasureScopeImpl<'_>,
constraints: Constraints,
inputs: &WearScalingListInputs,
transforms: &Rc<RefCell<Vec<WearItemTransform>>>,
layout: &Rc<RefCell<WearScalingLayoutInfo>>,
heights: &Rc<RefCell<ItemHeights>>,
indicator: &Rc<RefCell<IndicatorState>>,
) -> MeasureResult {
let spec = inputs.spec;
let top_aligned = spec.auto_centering.is_none();
let scale = WearDensity::current().density();
let density = WearDensity::new(scale, 1.0);
let width = if constraints.max_width.is_finite() {
constraints.max_width
} else {
constraints.min_width
};
let viewport = if constraints.max_height.is_finite() {
constraints.max_height
} else {
constraints.min_height
};
let inset = density.dp(spec.content_padding_start) + density.dp(spec.content_padding_end);
let item_width = (width - inset).max(0.0);
let child_constraints = Constraints {
min_width: 0.0,
max_width: item_width,
min_height: 0.0,
max_height: f32::INFINITY,
};
let spacing = density.dp(spec.item_spacing);
let count = inputs.content.items.len();
transforms
.borrow_mut()
.resize_with(count, WearItemTransform::new);
heights.borrow_mut().resize(count);
if count == 0 {
*layout.borrow_mut() = WearScalingLayoutInfo {
item_count: 0,
viewport,
..WearScalingLayoutInfo::default()
};
indicator.borrow_mut().clear();
return scope
.layout_with_placement_builder(width, viewport, |placements| placements.clear());
}
scope.set_reusable_pool_limits(REUSABLE_SLOTS, REUSABLE_SLOTS);
let anchor = inputs.anchor;
let start = if top_aligned {
0
} else {
anchor.index.min(count - 1)
};
let mut window: Vec<WindowedRow> = Vec::new();
let anchored = compose_and_measure_item(
scope,
start,
inputs,
transforms,
heights,
&density,
child_constraints,
);
let anchored_top = if top_aligned {
density.dp(spec.content_padding_top)
} else {
round_to_px(
viewport * 0.5 - anchored.height * 0.5 - anchor.offset,
scale,
)
};
let anchored_height = anchored.height;
let place = |top: f32, height: f32| {
place_row_with(spec.scaling, viewport, top, height, scale).unwrap_or(PlacedRow {
top,
height,
reported_height: height,
scale: 1.0,
alpha: 1.0,
})
};
let anchored_placed = place(anchored_top, anchored_height);
window.push(WindowedRow {
index: start,
roots: anchored.roots,
top: anchored_top,
height: anchored_height,
placed: anchored_placed,
});
let mut edge = anchored_top;
let mut budget = spec.beyond_bounds_item_count;
let mut index = start;
while index > 0 {
index -= 1;
let bottom = edge - spacing;
if bottom <= 0.0 {
if budget == 0 {
break;
}
budget -= 1;
}
let item = compose_and_measure_item(
scope,
index,
inputs,
transforms,
heights,
&density,
child_constraints,
);
let top = bottom - item.height;
let placed = place(top, item.height);
edge = bottom - placed.reported_height;
window.push(WindowedRow {
index,
roots: item.roots,
top,
height: item.height,
placed,
});
}
let mut edge = anchored_top + anchored_placed.reported_height;
let mut budget = spec.beyond_bounds_item_count;
for index in (start + 1)..count {
let top = edge + spacing;
if top >= viewport {
if budget == 0 {
break;
}
budget -= 1;
}
let item = compose_and_measure_item(
scope,
index,
inputs,
transforms,
heights,
&density,
child_constraints,
);
let placed = place(top, item.height);
edge = top + placed.reported_height;
window.push(WindowedRow {
index,
roots: item.roots,
top,
height: item.height,
placed,
});
}
window.sort_unstable_by_key(|row| row.index);
let composed = window.len();
let left = density.dp(spec.content_padding_start);
let mut visible = 0usize;
let result = {
let handles = transforms.borrow();
scope.layout_with_placement_builder(width, viewport, |placements| {
placements.clear();
for item in &window {
let row = item.placed;
if let Some(transform) = handles.get(item.index) {
transform.set(ScaleAlpha {
scale: row.scale,
alpha: row.alpha,
});
}
if row.top >= viewport || row.top + row.height <= 0.0 {
continue;
}
visible += 1;
for &(node_id, offset, root_width) in &item.roots {
let x = left + density.centre(item_width, root_width);
placements.push(Placement::new(node_id, x, row.top + offset, 0));
}
}
})
};
let known = heights.borrow();
indicator
.borrow_mut()
.record(&window, &spec, &known, viewport, density);
let before: f32 = (0..start).map(|i| known.height_of(i) + spacing).sum();
let after: f32 = ((start + 1)..count)
.map(|i| spacing + known.height_of(i))
.sum();
let first_top = anchored_top - before;
let last_bottom = anchored_top + anchored_height + after;
*layout.borrow_mut() = WearScalingLayoutInfo {
item_count: count,
viewport,
first_centre: first_top + known.height_of(0) * 0.5,
last_centre: last_bottom - known.height_of(count - 1) * 0.5,
content: last_bottom - first_top,
visible,
composed,
};
result
}
const REUSABLE_SLOTS: usize = 32;
struct MeasuredItem {
roots: Vec<(NodeId, f32, f32)>,
height: f32,
}
fn compose_and_measure_item(
scope: &mut SubcomposeMeasureScopeImpl<'_>,
index: usize,
inputs: &WearScalingListInputs,
transforms: &Rc<RefCell<Vec<WearItemTransform>>>,
heights: &Rc<RefCell<ItemHeights>>,
density: &WearDensity,
child_constraints: Constraints,
) -> MeasuredItem {
let transform = transforms
.borrow()
.get(index)
.cloned()
.unwrap_or_else(WearItemTransform::new);
let strategy = inputs.spec.compositing_strategy;
let item = inputs.content.items[index].clone();
let children: Vec<SubcomposeChild> = scope.subcompose(SlotId(index as u64), move || {
let item = item.clone();
WearScalingItem(Modifier::empty(), transform.clone(), strategy, move || {
item()
});
});
let mut roots = Vec::with_capacity(children.len());
let mut stacked = 0.0f32;
for child in children {
let placeable = scope.measure(child, child_constraints);
roots.push((placeable.node_id(), stacked, placeable.width()));
stacked += placeable.height();
}
let height = density.ceil(stacked);
heights.borrow_mut().record(index, height);
MeasuredItem { roots, height }
}
#[cfg(test)]
mod tests {
use super::*;
fn info(count: usize, viewport: f32, first: f32, last: f32) -> WearScalingLayoutInfo {
WearScalingLayoutInfo {
item_count: count,
viewport,
first_centre: first,
last_centre: last,
content: last - first,
visible: count,
composed: count,
}
}
#[test]
fn a_transform_handle_is_the_cell_not_the_value() {
let one = WearItemTransform::new();
let shared = one.clone();
let other = WearItemTransform::new();
assert_eq!(one, shared);
assert_ne!(one, other, "two fresh cells are two channels");
shared.set(ScaleAlpha {
scale: 0.7,
alpha: 0.5,
});
assert_eq!(one.get().scale, 0.7, "a clone writes through");
}
#[test]
fn travel_is_measured_between_the_first_and_last_centres() {
let info = info(3, 454.0, 227.0, 627.0);
assert_eq!(info.travel(), 400.0);
assert_eq!(info.scrolled(), 0.0, "at rest the first row is centred");
}
#[test]
fn scrolling_past_an_end_stops_instead_of_counting_on() {
let info = info(3, 454.0, 227.0, 627.0);
let anchor = CentreAnchor {
index: 0,
offset: 0.0,
};
let up = re_anchor(anchor, -50.0, info);
assert_eq!(up.offset, 0.0, "already at the top");
let down = re_anchor(anchor, 120.0, info);
assert_eq!(down.offset, 120.0);
let past = re_anchor(anchor, 900.0, info);
assert_eq!(past.offset, 400.0, "clamped to the whole travel");
}
#[test]
fn an_empty_list_does_not_divide_by_its_own_travel() {
let empty = WearScalingLayoutInfo::default();
assert_eq!(empty.travel(), 0.0);
assert_eq!(empty.scrolled(), 0.0);
let anchor = CentreAnchor::default();
assert_eq!(re_anchor(anchor, 30.0, empty).offset, 30.0);
}
#[test]
fn the_spec_defaults_are_the_ones_wear_ships() {
let spec = WearScalingLazyColumnSpec::default();
assert_eq!(spec.item_spacing, 4.0, "Arrangement.spacedBy(4.dp)");
assert_eq!(
spec.auto_centering,
Some(CentreAnchor {
index: 1,
offset: 0.0
}),
"AutoCenteringParams(itemIndex = 1, itemOffset = 0)"
);
assert_eq!(spec.scaling, ScalingParams::WEAR);
}
}