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, StretchAxis, SubView,
VerticalAlignment, ViewDimensions,
container::{FixedContainer, LazyContainer},
measure_layout,
scroll::Axis,
stack::LazyStackAxis,
with_memoized_children,
};
use crate::{IntoFFI, IntoRust, WuiAnyView, array::WuiArray};
use crate::{WuiTypeId, views::WuiAnyViews};
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()
}
}
#[unsafe(no_mangle)]
pub extern "C" fn waterui_spacer_id() -> WuiTypeId {
WuiTypeId::of::<waterui::component::spacer::Spacer>()
}
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_finite() {
Some(self.width)
} else {
None
},
height: if self.height.is_finite() {
Some(self.height)
} else {
None
},
}
}
}
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);
#[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(
layout: *mut WuiLayout,
bounds: WuiRect,
mut children: WuiArray<WuiSubView>,
) -> WuiArray<WuiRect> {
let layout: &dyn Layout = unsafe { &*(*layout).0 };
let bounds = unsafe { bounds.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 rects = with_memoized_children(&subview_refs, |refs| layout.place(bounds, refs));
children.consume();
rects.into_ffi()
}
#[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 core::cell::Cell;
use nami::{Computed, SignalExt, binding};
use waterui_layout::stack::{HStackLayout, VStackLayout};
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) };
}
}