#![allow(non_snake_case)]
use std::{
cell::{Cell, RefCell},
hash::{DefaultHasher, Hash, Hasher},
rc::Rc,
};
use cranpose_core::{
MutableState, NodeId, SlotId, internal::FrameCallbackRegistration, remember,
rememberMutableStateOf,
};
use cranpose_foundation::{
DRAG_THRESHOLD, MAX_FLING_VELOCITY, VelocityTracker1D,
lazy::{LazyItems, LazyLayoutKey},
};
use cranpose_ui_graphics::{CompositingStrategy, Point, Rect, Size};
use cranpose_ui_layout::{
Constraints, Measurable, MeasurePolicy, MeasureResult, MeasureScope, Placement,
};
use crate::{
composable,
density::Density,
fling_animation::FlingAnimation,
modifier::{GraphicsLayer, Modifier, PointerEventKind, PointerInputScope, TransformOrigin},
round_scaling_list::{
CentreAnchor, PlacedRow, ScaleAlpha, ScalingParams, leading_auto_centring_spacer,
place_row_with, round_to_px, trailing_auto_centring_spacer,
},
round_scroll_indicator::{IndicatorItem, ScalingList, ThumbLength, scaling_list_items_with},
subcompose_layout::{
MeasurePolicy as SubcomposeMeasurePolicy, SubcomposeChild, SubcomposeLayoutNode,
SubcomposeMeasureScope, SubcomposeMeasureScopeImpl,
},
widgets::Layout,
};
#[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,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct WearScalingItemInfo {
pub index: usize,
pub bounds: Rect,
pub centre: f32,
pub unscaled_height: f32,
pub scale: f32,
pub alpha: f32,
}
impl WearScalingItemInfo {
pub fn contains(self, point: Point) -> bool {
point.x >= self.bounds.x
&& point.x < self.bounds.x + self.bounds.width
&& point.y >= self.bounds.y
&& point.y < self.bounds.y + self.bounds.height
}
}
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, Copy)]
pub struct WearScalingListState {
anchor: MutableState<CentreAnchor>,
inner: MutableState<Rc<WearScalingListInner>>,
}
struct WearScalingListInner {
layout: Rc<RefCell<WearScalingLayoutInfo>>,
items: Rc<RefCell<Vec<WearScalingItemInfo>>>,
heights: Rc<RefCell<ItemHeights>>,
indicator: Rc<RefCell<IndicatorState>>,
scroll_animation: RefCell<Option<FrameCallbackRegistration>>,
summary: MutableState<WearScalingListSummary>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct WearScalingListSummary {
pub item_count: usize,
pub visible_item_count: usize,
pub can_scroll_forward: bool,
pub can_scroll_backward: bool,
}
impl PartialEq for WearScalingListState {
fn eq(&self, other: &Self) -> bool {
self.inner == other.inner
}
}
#[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: Density,
) {
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 {
fn inner(&self) -> Rc<WearScalingListInner> {
self.inner.get_non_reactive()
}
fn id(&self) -> u64 {
let mut hasher = DefaultHasher::new();
self.inner.runtime_state_id().hash(&mut hasher);
hasher.finish()
}
pub fn anchor(&self) -> CentreAnchor {
self.anchor.get()
}
pub fn set_anchor(&self, anchor: CentreAnchor) {
self.anchor.set(anchor);
}
pub fn cancel_scroll_animation(&self) {
self.inner().scroll_animation.borrow_mut().take();
}
pub fn scroll_by(&self, delta: f32) -> f32 {
if !delta.is_finite() {
return 0.0;
}
let inner = self.inner();
inner.scroll_animation.borrow_mut().take();
let info = *inner.layout.borrow();
let anchor = self.anchor.get();
let available_before = info.scrolled();
let available_after = (info.travel() - available_before).max(0.0);
let applied = delta.clamp(-available_before, available_after);
if applied == 0.0 {
return 0.0;
}
let items = inner.items.borrow();
let target = info.viewport * 0.5 + applied;
let next = items
.iter()
.min_by(|left, right| {
(left.centre - target)
.abs()
.total_cmp(&(right.centre - target).abs())
})
.map_or_else(
|| re_anchor(anchor, applied, info),
|item| CentreAnchor {
index: item.index,
offset: info.viewport * 0.5 - item.centre + applied,
},
);
drop(items);
self.set_anchor(next);
applied
}
pub fn dispatch_raw_delta(&self, delta: f32) -> f32 {
self.scroll_by(delta)
}
pub fn layout_info(&self) -> WearScalingLayoutInfo {
*self.inner().layout.borrow()
}
pub fn summary(&self) -> WearScalingListSummary {
self.inner().summary.value()
}
pub fn item_count(&self) -> usize {
self.summary().item_count
}
pub fn visible_item_count(&self) -> usize {
self.summary().visible_item_count
}
pub fn can_scroll_forward(&self) -> bool {
self.summary().can_scroll_forward
}
pub fn can_scroll_backward(&self) -> bool {
self.summary().can_scroll_backward
}
pub fn scroll_to_item(&self, index: usize, offset: f32) {
self.cancel_scroll_animation();
let count = self.inner().heights.borrow().len();
let index = if count == 0 {
index
} else {
index.min(count - 1)
};
self.set_anchor(CentreAnchor {
index,
offset: if offset.is_finite() { offset } else { 0.0 },
});
}
pub fn distance_to_item(&self, index: usize, offset: f32) -> f32 {
let inner = self.inner();
let info = *inner.layout.borrow();
let centre_line = info.viewport * 0.5;
let offset = if offset.is_finite() { offset } else { 0.0 };
if let Some(item) = inner
.items
.borrow()
.iter()
.find(|item| item.index == index)
.copied()
{
return item.centre - offset - centre_line;
}
let anchor = self.anchor.get_non_reactive();
let heights = inner.heights.borrow();
let spacing_free = signed_span(&heights, anchor.index, index);
spacing_free - anchor.offset - offset
}
pub fn contains_item(&self, index: usize) -> bool {
index < self.inner().heights.borrow().len()
}
pub fn animate_scroll_to_item(&self, index: usize, offset: f32) {
self.cancel_scroll_animation();
if !self.contains_item(index) {
return;
}
self.step_scroll_animation(index, if offset.is_finite() { offset } else { 0.0 });
}
fn step_scroll_animation(&self, index: usize, offset: f32) {
let Some(runtime) = cranpose_core::current_runtime_handle() else {
self.scroll_to_item(index, offset);
return;
};
let state = *self;
let registration = runtime.frame_clock().with_frame_nanos(move |_| {
let inner = state.inner();
inner.scroll_animation.borrow_mut().take();
let remaining = state.distance_to_item(index, offset);
if remaining.abs() <= SCROLL_ANIMATION_EPSILON {
state.scroll_to_item(index, offset);
return;
}
let step = remaining * SCROLL_ANIMATION_FRACTION;
let step = if step.abs() < SCROLL_ANIMATION_MIN_STEP {
SCROLL_ANIMATION_MIN_STEP.copysign(remaining)
} else {
step
};
let applied = state.scroll_by(step);
if applied == 0.0 {
return;
}
state.step_scroll_animation(index, offset);
});
*self.inner().scroll_animation.borrow_mut() = Some(registration);
}
pub fn visible_items(&self) -> Vec<WearScalingItemInfo> {
self.inner().items.borrow().clone()
}
pub fn item_at(&self, point: Point) -> Option<WearScalingItemInfo> {
self.inner()
.items
.borrow()
.iter()
.rev()
.copied()
.find(|item| item.contains(point))
}
pub fn with_indicator_list<R>(
&self,
read: impl FnOnce(&mut ThumbLength, ScalingList<'_>) -> R,
) -> R {
let inner = self.inner();
let mut indicator = inner.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
}
const SCROLL_ANIMATION_FRACTION: f32 = 0.22;
const SCROLL_ANIMATION_MIN_STEP: f32 = 1.0;
const SCROLL_ANIMATION_EPSILON: f32 = 0.5;
fn signed_span(heights: &ItemHeights, from: usize, to: usize) -> f32 {
if from == to {
return 0.0;
}
let (low, high) = if from < to { (from, to) } else { (to, from) };
let span: f32 = (low..high).map(|index| heights.height_of(index)).sum();
if from < to { span } else { -span }
}
#[composable]
#[track_caller]
pub fn rememberWearScalingListState(initial: CentreAnchor) -> WearScalingListState {
let anchor = rememberMutableStateOf(move || initial);
let inner = remember(|| {
let runtime = cranpose_core::current_runtime_handle()
.expect("rememberWearScalingListState requires an active runtime");
MutableState::with_runtime(
Rc::new(WearScalingListInner {
layout: Rc::new(RefCell::new(WearScalingLayoutInfo::default())),
items: Rc::new(RefCell::new(Vec::new())),
heights: Rc::new(RefCell::new(ItemHeights::default())),
indicator: Rc::new(RefCell::new(IndicatorState::default())),
scroll_animation: RefCell::new(None),
summary: MutableState::with_runtime(
WearScalingListSummary::default(),
runtime.clone(),
),
}),
runtime,
)
})
.with(|state| *state);
WearScalingListState { anchor, inner }
}
#[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<WearScalingListItem>,
}
#[derive(Clone)]
pub(crate) struct WearScalingListItem {
pub(crate) key: Option<u64>,
pub(crate) content_type: Option<u64>,
pub(crate) content: Rc<dyn Fn()>,
}
impl WearScalingListScope {
pub fn item_keyed<F>(&mut self, key: Option<u64>, content_type: Option<u64>, content: F)
where
F: Fn() + 'static,
{
self.items.push(WearScalingListItem {
key,
content_type,
content: Rc::new(content),
});
}
pub fn item<F>(&mut self, content: F)
where
F: Fn() + 'static,
{
self.item_keyed(None, None, content);
}
pub fn items<I, F>(&mut self, items: I, item: F)
where
I: Into<LazyItems>,
F: Fn(usize) + Clone + 'static,
{
let items_spec = items.into();
let key = items_spec.key_fn();
let content_type = items_spec.content_type_fn();
for index in 0..items_spec.count() {
let item = item.clone();
self.item_keyed(
key.as_ref().map(|key| key(index)),
content_type
.as_ref()
.map(|content_type| content_type(index)),
move || item(index),
);
}
}
pub fn count(&self) -> usize {
self.items.len()
}
}
#[derive(Clone)]
pub struct WearScalingListContent {
items: Rc<Vec<WearScalingListItem>>,
}
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()),
}
}
}
fn wear_scaling_list_input(
modifier: Modifier,
state: WearScalingListState,
fling: Rc<FlingAnimation>,
) -> Modifier {
let rotary_state = state;
let touch_state = state;
let touch_fling = Rc::clone(&fling);
modifier
.on_rotary_scroll_event(move |event| {
let delta = if event.vertical_scroll_pixels != 0.0 {
event.vertical_scroll_pixels
} else {
event.horizontal_scroll_pixels
};
rotary_state.dispatch_raw_delta(delta) != 0.0
})
.pointer_input(state.id(), move |scope: PointerInputScope| {
let fling = Rc::clone(&touch_fling);
async move {
scope
.await_pointer_event_scope(|events| async move {
let mut pointer = None;
let mut down = Point::new(0.0, 0.0);
let mut last = Point::new(0.0, 0.0);
let mut dragging = false;
let mut velocity = VelocityTracker1D::new();
loop {
let event = events.await_pointer_event().await;
match event.kind {
PointerEventKind::Down if pointer.is_none() => {
fling.cancel();
pointer = Some(event.id);
down = event.position;
last = event.position;
dragging = false;
velocity.reset();
if let Some(time) = event.time_ms {
velocity.add_data_point(time, event.position.y);
}
}
PointerEventKind::Move if pointer == Some(event.id) => {
if event.is_consumed() {
pointer = None;
dragging = false;
velocity.reset();
continue;
}
let dx = event.position.x - down.x;
let dy = event.position.y - down.y;
if !dragging && dy.abs() > DRAG_THRESHOLD && dy.abs() > dx.abs()
{
dragging = true;
} else if !dragging
&& dx.abs() > DRAG_THRESHOLD
&& dx.abs() >= dy.abs()
{
pointer = None;
velocity.reset();
continue;
}
if dragging {
let consumed = touch_state
.dispatch_raw_delta(last.y - event.position.y);
last = event.position;
if let Some(time) = event.time_ms {
velocity.add_data_point(time, event.position.y);
}
if consumed != 0.0 {
event.consume();
}
}
}
PointerEventKind::Up if pointer == Some(event.id) => {
if dragging {
if let Some(time) = event.time_ms {
velocity.add_data_point(time, event.position.y);
}
let speed = -velocity
.calculate_velocity_with_max(MAX_FLING_VELOCITY);
let fling_state = touch_state;
fling.start_fling(
0.0,
speed,
move |delta| fling_state.dispatch_raw_delta(delta),
|| {},
);
event.consume();
}
pointer = None;
dragging = false;
velocity.reset();
}
PointerEventKind::Cancel if pointer == Some(event.id) => {
pointer = None;
dragging = false;
velocity.reset();
}
PointerEventKind::Scroll => {
let consumed =
touch_state.dispatch_raw_delta(event.scroll_delta.y);
if consumed != 0.0 {
event.consume();
}
}
_ => {}
}
}
})
.await;
}
})
}
#[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,
scope: &dyn MeasureScope,
measurables: &[Box<dyn Measurable>],
constraints: Constraints,
) -> MeasureResult {
let mut placements = Vec::new();
let size = self.measure_into(scope, measurables, constraints, &mut placements);
MeasureResult::new(size, placements)
}
fn measure_into(
&self,
_scope: &dyn MeasureScope,
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 fling = remember(|| {
let runtime = cranpose_core::current_runtime_handle()
.expect("WearScalingLazyColumn requires an active runtime");
Rc::new(FlingAnimation::new(runtime))
})
.with(Rc::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 inner = state.inner();
let layout = Rc::clone(&inner.layout);
let items = Rc::clone(&inner.items);
let heights = Rc::clone(&inner.heights);
let indicator = Rc::clone(&inner.indicator);
let outputs = WearScalingMeasureOutputs {
transforms,
layout,
items,
heights,
indicator,
summary: inner.summary,
};
move || {
let policy: Rc<SubcomposeMeasurePolicy> = Rc::new(
move |scope: &mut SubcomposeMeasureScopeImpl<'_>, constraints: Constraints| {
measure_wear_scaling_list(scope, constraints, &inputs.borrow(), &outputs)
},
);
policy
}
})
.with(|policy| policy.clone());
let modifier = wear_scaling_list_input(modifier, state, fling).clip_to_bounds();
let list_id = state.id();
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());
let composed_density = crate::density::density();
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);
node.set_density(composed_density);
if inputs_changed {
node.invalidate_subcomposition();
}
}) {
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,
}
struct WearScalingMeasureOutputs {
transforms: Rc<RefCell<Vec<WearItemTransform>>>,
layout: Rc<RefCell<WearScalingLayoutInfo>>,
items: Rc<RefCell<Vec<WearScalingItemInfo>>>,
heights: Rc<RefCell<ItemHeights>>,
indicator: Rc<RefCell<IndicatorState>>,
summary: MutableState<WearScalingListSummary>,
}
impl WearScalingMeasureOutputs {
fn publish(&self, info: WearScalingLayoutInfo) {
*self.layout.borrow_mut() = info;
let travel = info.travel();
let scrolled = info.scrolled();
let summary = WearScalingListSummary {
item_count: info.item_count,
visible_item_count: info.visible,
can_scroll_forward: travel - scrolled > SCROLL_EPSILON,
can_scroll_backward: scrolled > SCROLL_EPSILON,
};
if self.summary.get_non_reactive() != summary {
self.summary.set(summary);
}
}
}
const SCROLL_EPSILON: f32 = 0.5;
fn measure_wear_scaling_list(
scope: &mut SubcomposeMeasureScopeImpl<'_>,
constraints: Constraints,
inputs: &WearScalingListInputs,
outputs: &WearScalingMeasureOutputs,
) -> MeasureResult {
let WearScalingMeasureOutputs {
transforms,
items,
heights,
indicator,
..
} = outputs;
let spec = inputs.spec;
let top_aligned = spec.auto_centering.is_none();
let scale = scope.density();
let density = Density::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 {
items.borrow_mut().clear();
outputs.publish(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 - item.height;
window.push(WindowedRow {
index,
roots: item.roots,
top,
height: item.height,
placed,
});
}
let mut edge = anchored_top + anchored_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 + item.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();
let mut placed_items = items.borrow_mut();
placed_items.clear();
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;
placed_items.push(WearScalingItemInfo {
index: item.index,
bounds: Rect {
x: left + item_width * (1.0 - row.scale) * 0.5,
y: row.top,
width: item_width * row.scale,
height: row.height,
},
centre: item.top + item.height * 0.5,
unscaled_height: item.height,
scale: row.scale,
alpha: row.alpha,
});
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;
outputs.publish(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: &Density,
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 key = match item.key {
Some(key) => LazyLayoutKey::User(key),
None => LazyLayoutKey::Index(index),
};
let slot_id = SlotId(key.to_slot_id());
let identity = item.key.map(|_| key.to_slot_id());
scope.update_content_type(slot_id, item.content_type);
let content = Rc::clone(&item.content);
let children: Vec<SubcomposeChild> = scope.subcompose(slot_id, (), move || {
let content = Rc::clone(&content);
crate::lazy_item::ProvideLazyItemKey(identity, || {
WearScalingItem(Modifier::empty(), transform.clone(), strategy, move || {
content()
});
});
});
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 a_declared_row_carries_its_key_and_content_type() {
let mut scope = WearScalingListScope::default();
scope.item_keyed(Some(7), Some(1), || {});
scope.items(
LazyItems::new(3)
.key(|index: usize| 100 + index as u64)
.content_type(|index: usize| (index % 2) as u64),
|_| {},
);
assert_eq!(scope.count(), 4);
assert_eq!(scope.items[0].key, Some(7));
assert_eq!(scope.items[0].content_type, Some(1));
assert_eq!(
scope.items[1..]
.iter()
.map(|item| item.key)
.collect::<Vec<_>>(),
[Some(100), Some(101), Some(102)]
);
assert_eq!(
scope.items[1..]
.iter()
.map(|item| item.content_type)
.collect::<Vec<_>>(),
[Some(0), Some(1), Some(0)]
);
}
#[test]
fn a_row_declared_without_a_key_is_identified_by_its_position() {
let mut scope = WearScalingListScope::default();
scope.items(2, |_| {});
assert!(scope.items.iter().all(|item| item.key.is_none()));
assert!(scope.items.iter().all(|item| item.content_type.is_none()));
}
#[test]
fn a_user_key_and_an_index_never_name_the_same_slot() {
assert_ne!(
LazyLayoutKey::User(3).to_slot_id(),
LazyLayoutKey::Index(3).to_slot_id()
);
}
fn summary_for(info: WearScalingLayoutInfo) -> (bool, bool) {
let travel = info.travel();
let scrolled = info.scrolled();
(
travel - scrolled > SCROLL_EPSILON,
scrolled > SCROLL_EPSILON,
)
}
#[test]
fn a_list_at_its_top_can_only_scroll_forward() {
let (forward, backward) = summary_for(info(10, 200.0, 100.0, 500.0));
assert!(forward);
assert!(!backward);
}
#[test]
fn a_list_at_its_end_can_only_scroll_backward() {
let (forward, backward) = summary_for(info(10, 200.0, -300.0, 100.0));
assert!(!forward);
assert!(backward);
}
#[test]
fn a_list_that_fits_can_scroll_neither_way() {
let (forward, backward) = summary_for(info(1, 200.0, 100.0, 100.0));
assert!(!forward);
assert!(!backward);
}
#[test]
fn an_unmeasured_height_is_the_mean_of_the_measured_ones() {
let mut heights = ItemHeights::default();
heights.resize(4);
heights.record(0, 30.0);
heights.record(1, 50.0);
assert_eq!(heights.height_of(0), 30.0);
assert_eq!(heights.height_of(3), 40.0, "the mean of 30 and 50");
assert_eq!(signed_span(&heights, 0, 2), 80.0);
assert_eq!(signed_span(&heights, 2, 0), -80.0);
assert_eq!(signed_span(&heights, 2, 2), 0.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);
}
}