use crate::components::text::WuiHorizontalAlignment;
use alloc::{boxed::Box, rc::Rc, vec::Vec};
use core::ffi::c_void;
use core::fmt;
use nami::{Signal, SignalExt};
use waterui_layout::{
HorizontalAlignment, Layout, Point, ProposalSize, Rect, ScrollView, Size, Spacer, StretchAxis,
SubView, SubviewPlacement, VerticalAlignment, ViewDimensions,
container::{FixedContainer, LazyContainer},
measure_layout,
scroll::Axis,
stack::LazyStackAxis,
with_memoized_children,
};
use crate::views::WuiAnyViews;
use crate::{IntoFFI, IntoRust, WuiAnyView, array::WuiArray};
opaque!(WuiLayout, Box<dyn Layout>, layout);
#[repr(C)]
pub struct WuiFixedContainer {
pub layout: *mut WuiLayout,
pub contents: WuiArray<*mut WuiAnyView>,
}
impl fmt::Debug for WuiFixedContainer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("WuiFixedContainer").finish_non_exhaustive()
}
}
#[repr(C)]
#[derive(Debug)]
pub struct WuiSpacer {
pub min_length: f32,
}
ffi_view!(Spacer, WuiSpacer, spacer);
impl IntoFFI for Spacer {
type FFI = WuiSpacer;
fn into_ffi(self) -> Self::FFI {
WuiSpacer {
min_length: self.min_length(),
}
}
}
ffi_view!(FixedContainer, WuiFixedContainer, fixed_container);
impl IntoFFI for FixedContainer {
type FFI = WuiFixedContainer;
fn into_ffi(self) -> Self::FFI {
let (layout, contents) = self.into_inner();
WuiFixedContainer {
layout: layout.into_ffi(),
contents: contents.into_ffi(),
}
}
}
#[repr(C)]
#[derive(Debug)]
pub struct WuiContainer {
pub layout: *mut WuiLayout,
pub contents: *mut WuiAnyViews,
}
ffi_view!(LazyContainer, WuiContainer, layout_container);
impl IntoFFI for LazyContainer {
type FFI = WuiContainer;
fn into_ffi(self) -> Self::FFI {
let (layout, contents) = self.into_inner();
WuiContainer {
layout: layout.into_ffi(),
contents: contents.into_ffi(),
}
}
}
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum WuiLazyStackAxis {
Unsupported = 0,
Vertical = 1,
Horizontal = 2,
}
#[derive(Clone, Copy)]
struct LazyStackDescriptor {
axis: WuiLazyStackAxis,
spacing: f32,
horizontal_alignment: WuiHorizontalAlignment,
vertical_alignment: WuiVerticalAlignment,
}
fn lazy_stack_descriptor(layout: &dyn Layout) -> Option<LazyStackDescriptor> {
waterui_layout::stack::lazy_stack_axis(layout).map(|axis| match axis {
LazyStackAxis::Vertical { spacing, alignment } => LazyStackDescriptor {
axis: WuiLazyStackAxis::Vertical,
spacing: spacing.get(),
horizontal_alignment: alignment.into_ffi(),
vertical_alignment: VerticalAlignment::Center.into_ffi(),
},
LazyStackAxis::Horizontal { spacing, alignment } => LazyStackDescriptor {
axis: WuiLazyStackAxis::Horizontal,
spacing: spacing.get(),
horizontal_alignment: HorizontalAlignment::Center.into_ffi(),
vertical_alignment: alignment.into_ffi(),
},
})
}
fn required_lazy_stack_descriptor(layout: &dyn Layout) -> LazyStackDescriptor {
lazy_stack_descriptor(layout)
.unwrap_or_else(|| panic!("waterui_layout_lazy_stack_* called for unsupported layout"))
}
pub type WuiLayoutInvalidationCallback = unsafe extern "C" fn(context: *mut c_void);
struct ForeignLayoutInvalidation {
context: *mut c_void,
invalidate: WuiLayoutInvalidationCallback,
drop: WuiLayoutInvalidationCallback,
}
impl ForeignLayoutInvalidation {
fn invalidate(&self) {
unsafe { (self.invalidate)(self.context) };
}
}
impl Drop for ForeignLayoutInvalidation {
fn drop(&mut self) {
unsafe { (self.drop)(self.context) };
}
}
pub struct WuiLayoutWatcher {
_guards: Vec<nami::watcher::BoxWatcherGuard>,
_target: Rc<ForeignLayoutInvalidation>,
}
impl fmt::Debug for WuiLayoutWatcher {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("WuiLayoutWatcher").finish_non_exhaustive()
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn waterui_layout_watch_invalidation(
layout: *const WuiLayout,
context: *mut c_void,
invalidate: WuiLayoutInvalidationCallback,
drop_callback: WuiLayoutInvalidationCallback,
) -> *mut WuiLayoutWatcher {
let layout = unsafe { crate::borrow_ffi(layout) };
let target = Rc::new(ForeignLayoutInvalidation {
context,
invalidate,
drop: drop_callback,
});
let callback_target = Rc::clone(&target);
let guards = layout.0.watch_invalidation(Rc::new(move || {
let target = Rc::clone(&callback_target);
target.invalidate();
}));
Box::into_raw(Box::new(WuiLayoutWatcher {
_guards: guards,
_target: target,
}))
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn waterui_layout_watcher_drop(watcher: *mut WuiLayoutWatcher) {
unsafe { drop(Box::from_raw(watcher)) };
}
#[derive(Clone, Default, Debug)]
#[repr(C)]
pub struct WuiProposalSize {
width: f32, height: f32,
}
impl IntoRust for WuiProposalSize {
type Rust = ProposalSize;
unsafe fn into_rust(self) -> Self::Rust {
ProposalSize {
width: if self.width.is_nan() {
None
} else {
Some(self.width)
},
height: if self.height.is_nan() {
None
} else {
Some(self.height)
},
}
}
}
impl IntoFFI for ProposalSize {
type FFI = WuiProposalSize;
fn into_ffi(self) -> Self::FFI {
WuiProposalSize {
width: self.width.unwrap_or(f32::NAN),
height: self.height.unwrap_or(f32::NAN),
}
}
}
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum WuiStretchAxis {
None = 0,
Horizontal = 1,
Vertical = 2,
Both = 3,
MainAxis = 4,
CrossAxis = 5,
}
impl From<WuiStretchAxis> for StretchAxis {
fn from(axis: WuiStretchAxis) -> Self {
match axis {
WuiStretchAxis::None => Self::None,
WuiStretchAxis::Horizontal => Self::Horizontal,
WuiStretchAxis::Vertical => Self::Vertical,
WuiStretchAxis::Both => Self::Both,
WuiStretchAxis::MainAxis => Self::MainAxis,
WuiStretchAxis::CrossAxis => Self::CrossAxis,
}
}
}
impl From<StretchAxis> for WuiStretchAxis {
fn from(axis: StretchAxis) -> Self {
match axis {
StretchAxis::None => Self::None,
StretchAxis::Horizontal => Self::Horizontal,
StretchAxis::Vertical => Self::Vertical,
StretchAxis::Both => Self::Both,
StretchAxis::MainAxis => Self::MainAxis,
StretchAxis::CrossAxis => Self::CrossAxis,
}
}
}
#[repr(C)]
#[derive(Debug)]
pub struct WuiSubViewVTable {
pub measure: unsafe extern "C" fn(
context: *mut core::ffi::c_void,
proposal: WuiProposalSize,
) -> WuiViewDimensions,
pub drop: unsafe extern "C" fn(context: *mut core::ffi::c_void),
}
#[repr(C)]
#[derive(Debug)]
pub struct WuiSubView {
pub context: *mut core::ffi::c_void,
pub vtable: WuiSubViewVTable,
pub stretch_axis: WuiStretchAxis,
pub priority: i32,
}
impl Drop for WuiSubView {
fn drop(&mut self) {
unsafe { (self.vtable.drop)(self.context) }
}
}
impl SubView for WuiSubView {
fn measure(&self, proposal: ProposalSize) -> ViewDimensions {
let result = unsafe { (self.vtable.measure)(self.context, proposal.into_ffi()) };
unsafe { result.into_rust() }
}
fn stretch_axis(&self) -> StretchAxis {
self.stretch_axis.into()
}
fn priority(&self) -> i32 {
self.priority
}
}
into_ffi! {Point,
pub struct WuiPoint {
x: f32,
y: f32,
}
}
impl IntoRust for WuiPoint {
type Rust = Point;
unsafe fn into_rust(self) -> Self::Rust {
Point {
x: self.x,
y: self.y,
}
}
}
into_ffi! {Size,
pub struct WuiSize {
width: f32,
height: f32,
}
}
impl IntoRust for WuiSize {
type Rust = Size;
unsafe fn into_rust(self) -> Self::Rust {
Size {
width: self.width,
height: self.height,
}
}
}
#[cfg(feature = "c-api")]
crate::ffi_computed!(Size, WuiSize, size);
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum WuiVerticalAlignment {
Top = 0,
#[default]
Center = 1,
Bottom = 2,
FirstBaseline = 3,
LastBaseline = 4,
}
impl IntoFFI for VerticalAlignment {
type FFI = WuiVerticalAlignment;
fn into_ffi(self) -> Self::FFI {
if self == Self::Top {
WuiVerticalAlignment::Top
} else if self == Self::Bottom {
WuiVerticalAlignment::Bottom
} else if self == Self::FirstBaseline {
WuiVerticalAlignment::FirstBaseline
} else if self == Self::LastBaseline {
WuiVerticalAlignment::LastBaseline
} else {
WuiVerticalAlignment::Center
}
}
}
impl IntoRust for WuiVerticalAlignment {
type Rust = VerticalAlignment;
unsafe fn into_rust(self) -> Self::Rust {
match self {
Self::Top => VerticalAlignment::Top,
Self::Center => VerticalAlignment::Center,
Self::Bottom => VerticalAlignment::Bottom,
Self::FirstBaseline => VerticalAlignment::FirstBaseline,
Self::LastBaseline => VerticalAlignment::LastBaseline,
}
}
}
#[derive(Clone, Copy, Default, Debug)]
#[repr(C)]
pub struct WuiHorizontalGuide {
alignment: WuiHorizontalAlignment,
value: f32,
}
impl IntoRust for WuiHorizontalGuide {
type Rust = (HorizontalAlignment, f32);
unsafe fn into_rust(self) -> Self::Rust {
(unsafe { self.alignment.into_rust() }, self.value)
}
}
#[derive(Clone, Copy, Default, Debug)]
#[repr(C)]
pub struct WuiVerticalGuide {
alignment: WuiVerticalAlignment,
value: f32,
}
impl IntoRust for WuiVerticalGuide {
type Rust = (VerticalAlignment, f32);
unsafe fn into_rust(self) -> Self::Rust {
(unsafe { self.alignment.into_rust() }, self.value)
}
}
#[repr(C)]
pub struct WuiViewDimensions {
size: WuiSize,
horizontal_guides: WuiArray<WuiHorizontalGuide>,
vertical_guides: WuiArray<WuiVerticalGuide>,
}
impl fmt::Debug for WuiViewDimensions {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("WuiViewDimensions")
.field("size", &self.size)
.finish_non_exhaustive()
}
}
impl IntoFFI for ViewDimensions {
type FFI = WuiViewDimensions;
fn into_ffi(self) -> Self::FFI {
let horizontal_guides = self
.explicit_horizontal_guides()
.map(|(alignment, value)| WuiHorizontalGuide {
alignment: alignment.into_ffi(),
value,
})
.collect::<Vec<_>>();
let vertical_guides = self
.explicit_vertical_guides()
.map(|(alignment, value)| WuiVerticalGuide {
alignment: alignment.into_ffi(),
value,
})
.collect::<Vec<_>>();
WuiViewDimensions {
size: self.size.into_ffi(),
horizontal_guides: WuiArray::new(horizontal_guides),
vertical_guides: WuiArray::new(vertical_guides),
}
}
}
impl IntoRust for WuiViewDimensions {
type Rust = ViewDimensions;
unsafe fn into_rust(self) -> Self::Rust {
let mut dimensions = ViewDimensions::new(unsafe { self.size.into_rust() });
for (alignment, value) in unsafe { self.horizontal_guides.into_rust() } {
dimensions.set_horizontal(alignment, value);
}
for (alignment, value) in unsafe { self.vertical_guides.into_rust() } {
dimensions.set_vertical(alignment, value);
}
dimensions
}
}
#[repr(C)]
#[derive(Debug)]
pub struct WuiRect {
origin: WuiPoint,
size: WuiSize,
}
impl IntoRust for WuiRect {
type Rust = Rect;
unsafe fn into_rust(self) -> Self::Rust {
unsafe { Rect::new(self.origin.into_rust(), self.size.into_rust()) }
}
}
impl IntoFFI for Rect {
type FFI = WuiRect;
fn into_ffi(self) -> Self::FFI {
WuiRect {
origin: self.origin().into_ffi(),
size: (*self.size()).into_ffi(),
}
}
}
#[cfg(feature = "c-api")]
crate::ffi_binding!(Rect, WuiRect, rect);
crate::ffi_watcher!(Rect, WuiRect, rect);
#[repr(C)]
#[derive(Debug)]
pub struct WuiSubviewPlacement {
frame: WuiRect,
proposal: WuiProposalSize,
}
impl IntoFFI for SubviewPlacement {
type FFI = WuiSubviewPlacement;
fn into_ffi(self) -> Self::FFI {
WuiSubviewPlacement {
frame: self.frame.into_ffi(),
proposal: self.proposal.into_ffi(),
}
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn waterui_layout_measure(
layout: *mut WuiLayout,
proposal: WuiProposalSize,
mut children: WuiArray<WuiSubView>,
) -> WuiViewDimensions {
let layout: &dyn Layout = unsafe { &*(*layout).0 };
let proposal = unsafe { proposal.into_rust() };
let children_slice = children.as_mut_slice();
let subview_refs: Vec<&dyn SubView> =
children_slice.iter().map(|s| s as &dyn SubView).collect();
let dimensions = measure_layout(layout, proposal, &subview_refs);
children.consume();
dimensions.into_ffi()
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn waterui_layout_place_subviews(
layout: *mut WuiLayout,
bounds: WuiRect,
proposal: WuiProposalSize,
mut children: WuiArray<WuiSubView>,
) -> WuiArray<WuiSubviewPlacement> {
let layout: &dyn Layout = unsafe { &*(*layout).0 };
let bounds = unsafe { bounds.into_rust() };
let proposal = unsafe { proposal.into_rust() };
let children_slice = children.as_mut_slice();
let subview_refs: Vec<&dyn SubView> =
children_slice.iter().map(|s| s as &dyn SubView).collect();
let placements =
with_memoized_children(&subview_refs, |refs| layout.place(bounds, proposal, refs));
children.consume();
placements.into_ffi()
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn waterui_layout_stretch_axis(
layout: *const WuiLayout,
children: WuiArray<WuiStretchAxis>,
) -> WuiStretchAxis {
let layout = unsafe { crate::borrow_ffi(layout) };
let axes: Vec<StretchAxis> = children
.as_slice()
.iter()
.copied()
.map(Into::into)
.collect();
let result = layout.0.stretch_axis(&axes);
children.consume();
result.into()
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn waterui_layout_lazy_stack_axis(
layout: *mut WuiLayout,
) -> WuiLazyStackAxis {
let layout: &dyn Layout = unsafe { &*(*layout).0 };
lazy_stack_descriptor(layout)
.map_or(WuiLazyStackAxis::Unsupported, |descriptor| descriptor.axis)
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn waterui_layout_lazy_stack_spacing(layout: *mut WuiLayout) -> f32 {
let layout: &dyn Layout = unsafe { &*(*layout).0 };
required_lazy_stack_descriptor(layout).spacing
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn waterui_layout_lazy_stack_horizontal_alignment(
layout: *mut WuiLayout,
) -> WuiHorizontalAlignment {
let layout: &dyn Layout = unsafe { &*(*layout).0 };
required_lazy_stack_descriptor(layout).horizontal_alignment
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn waterui_layout_lazy_stack_vertical_alignment(
layout: *mut WuiLayout,
) -> WuiVerticalAlignment {
let layout: &dyn Layout = unsafe { &*(*layout).0 };
required_lazy_stack_descriptor(layout).vertical_alignment
}
into_ffi! {Axis, non_exhaustive,
pub enum WuiAxis {
Horizontal,
Vertical,
All,
}
}
#[repr(C)]
#[derive(Debug)]
pub struct WuiScrollView {
pub axis: WuiAxis,
pub content: *mut WuiAnyView,
pub target_x: *mut crate::reactive::WuiComputed<f32>,
pub target_y: *mut crate::reactive::WuiComputed<f32>,
pub scroll_generation: *mut crate::reactive::WuiComputed<i32>,
}
impl IntoFFI for ScrollView {
type FFI = WuiScrollView;
fn into_ffi(self) -> Self::FFI {
let (axis, content, controller) = self.into_inner();
let (target_x, target_y, scroll_generation) = controller.map_or_else(
|| {
(
core::ptr::null_mut(),
core::ptr::null_mut(),
core::ptr::null_mut(),
)
},
|controller| {
let target = controller.target();
(
target.clone().map(|point| point.x).computed().into_ffi(),
target.map(|point| point.y).computed().into_ffi(),
controller.generation().into_ffi(),
)
},
);
WuiScrollView {
axis: axis.into_ffi(),
content: content.into_ffi(),
target_x,
target_y,
scroll_generation,
}
}
}
ffi_view!(ScrollView, WuiScrollView, scroll_view);
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
use core::cell::{Cell, RefCell};
use nami::{Computed, SignalExt, binding};
use waterui_layout::frame::FrameLayout;
use waterui_layout::stack::{HStackLayout, VStackLayout};
#[cfg(feature = "c-api")]
#[test]
fn layout_priority_metadata_preserves_values_and_content_ownership() {
use crate::{
waterui_force_as_metadata_layout_priority, waterui_metadata_layout_priority_id,
waterui_view_id,
};
use waterui_core::layout::LayoutPriority;
use waterui_core::{AnyView, Environment, Metadata, View};
struct DropProbe(Rc<Cell<usize>>);
impl Drop for DropProbe {
fn drop(&mut self) {
self.0.set(self.0.get() + 1);
}
}
impl View for DropProbe {
fn body(self, _env: &Environment) -> impl View {}
}
for priority in [i32::MIN, -3, 0, 5, i32::MAX] {
let drops = Rc::new(Cell::new(0));
let view = AnyView::new(Metadata::new(
DropProbe(Rc::clone(&drops)),
LayoutPriority::new(priority),
))
.into_ffi();
let view_id = unsafe { waterui_view_id(view) };
assert_eq!(view_id, waterui_metadata_layout_priority_id());
let metadata = unsafe { waterui_force_as_metadata_layout_priority(view) };
assert_eq!(metadata.value, priority);
assert_eq!(drops.get(), 0);
unsafe { drop(metadata.content.into_rust()) };
assert_eq!(drops.get(), 1);
}
}
#[cfg(feature = "c-api")]
#[test]
#[expect(
clippy::float_cmp,
reason = "min_length is copied verbatim across FFI with no intervening arithmetic, so exact equality is the correct assertion"
)]
fn spacer_crosses_ffi_with_its_minimum_length() {
use crate::waterui_view_id;
use waterui_core::{AnyView, Native};
let view = AnyView::new(Native::new(Spacer::new(40.0))).into_ffi();
let view_id = unsafe { waterui_view_id(view) };
assert_eq!(view_id, waterui_spacer_id());
let spacer = unsafe { waterui_force_as_spacer(view) };
assert_eq!(spacer.min_length, 40.0);
}
#[test]
fn layout_stretch_queries_follow_current_children() {
let check = |layout: *mut WuiLayout, main, cross| {
for (axes, expected) in [
(vec![], WuiStretchAxis::None),
(vec![WuiStretchAxis::MainAxis], main),
(vec![WuiStretchAxis::CrossAxis], cross),
(vec![WuiStretchAxis::Both], WuiStretchAxis::Both),
(vec![], WuiStretchAxis::None),
] {
let actual = unsafe { waterui_layout_stretch_axis(layout, WuiArray::new(axes)) };
assert_eq!(actual, expected);
}
};
with_layout(HStackLayout::default(), |layout| {
check(layout, WuiStretchAxis::Horizontal, WuiStretchAxis::Vertical);
});
with_layout(VStackLayout::default(), |layout| {
check(layout, WuiStretchAxis::Vertical, WuiStretchAxis::Horizontal);
});
}
fn with_layout(layout: impl Layout + 'static, f: impl FnOnce(*mut WuiLayout)) {
let mut layout = WuiLayout(Box::new(layout));
f(&raw mut layout);
}
#[test]
#[expect(
clippy::float_cmp,
reason = "spacing is copied verbatim across FFI from a Computed::constant with no intervening arithmetic, so exact equality is the correct assertion"
)]
fn lazy_stack_queries_report_vstack_configuration() {
with_layout(
VStackLayout {
alignment: HorizontalAlignment::Trailing,
spacing: Computed::constant(12.0),
},
|layout| unsafe {
assert_eq!(
waterui_layout_lazy_stack_axis(layout),
WuiLazyStackAxis::Vertical
);
assert_eq!(waterui_layout_lazy_stack_spacing(layout), 12.0);
assert_eq!(
waterui_layout_lazy_stack_horizontal_alignment(layout),
WuiHorizontalAlignment::Trailing
);
},
);
}
#[test]
#[expect(
clippy::float_cmp,
reason = "spacing is copied verbatim across FFI from a Computed::constant with no intervening arithmetic, so exact equality is the correct assertion"
)]
fn lazy_stack_queries_report_hstack_configuration() {
with_layout(
HStackLayout {
alignment: VerticalAlignment::Bottom,
spacing: Computed::constant(7.0),
},
|layout| unsafe {
assert_eq!(
waterui_layout_lazy_stack_axis(layout),
WuiLazyStackAxis::Horizontal
);
assert_eq!(waterui_layout_lazy_stack_spacing(layout), 7.0);
assert_eq!(
waterui_layout_lazy_stack_vertical_alignment(layout),
WuiVerticalAlignment::Bottom
);
},
);
}
#[test]
#[expect(
clippy::float_cmp,
reason = "spacing is copied verbatim across FFI from a Computed::constant with no intervening arithmetic, so exact equality is the correct assertion"
)]
fn layout_watcher_forwards_precise_signal_invalidation() {
struct Target(Rc<Cell<usize>>);
unsafe extern "C" fn invalidate(context: *mut c_void) {
let target = unsafe { &*(context as *const Target) };
target.0.set(target.0.get() + 1);
}
unsafe extern "C" fn drop_target(context: *mut c_void) {
unsafe { drop(Box::from_raw(context.cast::<Target>())) };
}
let spacing = binding(4.0_f32);
let mut layout = WuiLayout(Box::new(VStackLayout {
alignment: HorizontalAlignment::Center,
spacing: spacing.computed(),
}));
let invalidations = Rc::new(Cell::new(0));
let context = Box::into_raw(Box::new(Target(Rc::clone(&invalidations)))).cast();
let watcher = unsafe {
waterui_layout_watch_invalidation(&raw const layout, context, invalidate, drop_target)
};
spacing.set(12.0);
assert_eq!(invalidations.get(), 1);
assert_eq!(
unsafe { waterui_layout_lazy_stack_spacing(&raw mut layout) },
12.0
);
unsafe { waterui_layout_watcher_drop(watcher) };
}
fn proposal_cases() -> [Option<f32>; 4] {
[None, Some(0.0), Some(48.0), Some(f32::INFINITY)]
}
#[test]
fn proposal_round_trip_preserves_each_axis_probe() {
for width in proposal_cases() {
for height in proposal_cases() {
let proposal = ProposalSize::new(width, height);
let decoded = unsafe { proposal.into_ffi().into_rust() };
assert_eq!(decoded, proposal);
}
}
}
#[test]
fn proposal_decodes_nan_as_unspecified_without_losing_infinity() {
for bits in [f32::NAN.to_bits(), 0x7fc0_0001, 0xffc0_0042] {
let decoded = unsafe {
WuiProposalSize {
width: f32::from_bits(bits),
height: f32::INFINITY,
}
.into_rust()
};
assert_eq!(decoded, ProposalSize::new(None, Some(f32::INFINITY)));
}
let decoded = unsafe {
ProposalSize::new(Some(-0.0), Some(0.0))
.into_ffi()
.into_rust()
};
assert_eq!(decoded.width.unwrap().to_bits(), (-0.0_f32).to_bits());
assert_eq!(decoded.height.unwrap().to_bits(), 0.0_f32.to_bits());
}
fn probe_extent(proposed: Option<f32>, minimum: f32, ideal: f32, maximum: f32) -> f32 {
proposed.map_or(ideal, |value| value.clamp(minimum, maximum))
}
#[derive(Debug)]
struct ProbeView;
impl SubView for ProbeView {
fn measure(&self, proposal: ProposalSize) -> ViewDimensions {
ViewDimensions::new(Size::new(
probe_extent(proposal.width, 8.0, 24.0, 96.0),
probe_extent(proposal.height, 12.0, 36.0, 144.0),
))
.with_horizontal(HorizontalAlignment::Leading, 3.0)
.with_vertical(VerticalAlignment::FirstBaseline, 5.0)
}
fn stretch_axis(&self) -> StretchAxis {
StretchAxis::Both
}
fn priority(&self) -> i32 {
7
}
}
struct ProbeContext {
proposals: Rc<RefCell<Vec<ProposalSize>>>,
drops: Rc<Cell<usize>>,
}
unsafe extern "C" fn measure_probe(
context: *mut c_void,
proposal: WuiProposalSize,
) -> WuiViewDimensions {
let context = unsafe { &*context.cast::<ProbeContext>() };
let decoded = ProposalSize::new(
(!proposal.width.is_nan()).then_some(proposal.width),
(!proposal.height.is_nan()).then_some(proposal.height),
);
context.proposals.borrow_mut().push(decoded);
ProbeView.measure(decoded).into_ffi()
}
unsafe extern "C" fn drop_probe(context: *mut c_void) {
let context = unsafe { Box::from_raw(context.cast::<ProbeContext>()) };
context.drops.set(context.drops.get() + 1);
}
fn foreign_probe(
proposals: Rc<RefCell<Vec<ProposalSize>>>,
drops: Rc<Cell<usize>>,
) -> WuiSubView {
WuiSubView {
context: Box::into_raw(Box::new(ProbeContext { proposals, drops })).cast(),
vtable: WuiSubViewVTable {
measure: measure_probe,
drop: drop_probe,
},
stretch_axis: WuiStretchAxis::Both,
priority: 7,
}
}
const WIDTH_EXTENTS: [(Option<f32>, f32); 4] = [
(None, 24.0),
(Some(0.0), 8.0),
(Some(48.0), 48.0),
(Some(f32::INFINITY), 96.0),
];
const HEIGHT_EXTENTS: [(Option<f32>, f32); 4] = [
(None, 36.0),
(Some(0.0), 12.0),
(Some(48.0), 48.0),
(Some(f32::INFINITY), 144.0),
];
fn expected_extent(table: [(Option<f32>, f32); 4], probe: Option<f32>) -> f32 {
table
.into_iter()
.find_map(|(proposal, extent)| (proposal == probe).then_some(extent))
.expect("the tables cover every `proposal_cases` entry")
}
#[test]
fn layout_measure_preserves_probes_through_foreign_callbacks() {
for width in proposal_cases() {
for height in proposal_cases() {
let proposal = ProposalSize::new(width, height);
let expected_size = Size::new(
expected_extent(WIDTH_EXTENTS, width),
expected_extent(HEIGHT_EXTENTS, height),
);
let direct = measure_layout(&FrameLayout::default(), proposal, &[&ProbeView]);
let proposals = Rc::new(RefCell::new(Vec::new()));
let drops = Rc::new(Cell::new(0));
with_layout(
FrameLayout::default(),
|layout| unsafe {
let measured = waterui_layout_measure(
layout,
proposal.into_ffi(),
WuiArray::new(vec![foreign_probe(
Rc::clone(&proposals),
Rc::clone(&drops),
)]),
)
.into_rust();
assert_eq!(measured.size, expected_size);
assert_eq!(measured.size, direct.size);
assert_eq!(
measured.explicit_horizontal(HorizontalAlignment::Leading),
Some(3.0)
);
assert_eq!(
measured.explicit_vertical(VerticalAlignment::FirstBaseline),
Some(5.0)
);
assert_eq!(proposals.borrow().first(), Some(&proposal));
assert_eq!(drops.get(), 1);
let bounds = Rect::new(Point::new(13.0, -9.0), expected_size);
let placed = waterui_layout_place_subviews(
layout,
bounds.into_ffi(),
proposal.into_ffi(),
WuiArray::new(vec![foreign_probe(
Rc::clone(&proposals),
Rc::clone(&drops),
)]),
);
let rects: Vec<Rect> = placed
.as_slice()
.iter()
.map(|placement| {
let frame = &placement.frame;
Rect::new(
Point::new(frame.origin.x, frame.origin.y),
Size::new(frame.size.width, frame.size.height),
)
})
.collect();
for placement in placed.as_slice() {
let returned = placement.proposal.clone().into_rust();
assert_eq!(returned, proposal);
}
placed.consume();
assert_eq!(rects, vec![bounds]);
let direct: Vec<Rect> = FrameLayout::default()
.place(bounds, proposal, &[&ProbeView])
.into_iter()
.map(|placement| placement.frame)
.collect();
assert_eq!(rects, direct);
assert_eq!(drops.get(), 2);
},
);
}
}
}
#[test]
fn foreign_subview_preserves_metadata_and_measurement() {
let proposals = Rc::new(RefCell::new(Vec::new()));
let drops = Rc::new(Cell::new(0));
{
let subview = foreign_probe(Rc::clone(&proposals), Rc::clone(&drops));
assert_eq!(subview.priority(), 7);
assert_eq!(subview.stretch_axis(), StretchAxis::Both);
let proposal = ProposalSize::new(Some(f32::INFINITY), None);
let measured = subview.measure(proposal);
assert_eq!(measured.size, Size::new(96.0, 36.0));
assert_eq!(
measured.explicit_horizontal(HorizontalAlignment::Leading),
Some(3.0)
);
assert_eq!(
measured.explicit_vertical(VerticalAlignment::FirstBaseline),
Some(5.0)
);
assert_eq!(proposals.borrow().first(), Some(&proposal));
}
assert_eq!(drops.get(), 1);
}
}