Skip to main content

solium_layout/
ffi.rs

1//! A C ABI over the arrangements, so the preview can call them.
2//!
3//! Results are written into a fixed scratch buffer rather than allocated: the
4//! crate has no dependencies and wants none, and a wasm module that never
5//! allocates is a wasm module with nothing to leak. The caller reads the
6//! rectangles straight out of the module's memory.
7
8use crate::{Rect, Settings};
9
10/// The most windows an arrangement will report. Beyond this the extras are not
11/// laid out — a preview with sixty-five windows is not a case worth carrying an
12/// allocator for.
13pub const CAPACITY: usize = 64;
14
15/// Four `f64` per rectangle: x, y, w, h.
16static mut SCRATCH: [f64; CAPACITY * 4] = [0.0; CAPACITY * 4];
17
18/// Where the last arrangement was written.
19#[expect(unsafe_code, reason = "exporting a C symbol and its scratch buffer")]
20#[unsafe(no_mangle)]
21pub extern "C" fn solium_layout_scratch() -> *const f64 {
22    &raw const SCRATCH as *const f64
23}
24
25#[must_use]
26pub const fn capacity() -> usize {
27    CAPACITY
28}
29
30#[expect(unsafe_code, reason = "exporting a C symbol")]
31#[unsafe(no_mangle)]
32pub extern "C" fn solium_layout_capacity() -> u32 {
33    CAPACITY as u32
34}
35
36/// Copy `slots` into the scratch buffer and return how many were written.
37#[expect(unsafe_code, reason = "writing the scratch buffer the caller reads")]
38fn publish(slots: &[Rect]) -> u32 {
39    let written = slots.len().min(CAPACITY);
40    let scratch = &raw mut SCRATCH;
41    for (index, slot) in slots.iter().take(written).enumerate() {
42        // SAFETY: single-threaded by construction — this crate is called from
43        // the compositor's render thread or from a wasm module, never both, and
44        // `written` is clamped to the buffer's length above. Written through a
45        // raw pointer rather than a `&mut` to the static, so no reference to it
46        // exists that a concurrent read could alias.
47        unsafe {
48            (*scratch)[index * 4] = slot.x;
49            (*scratch)[index * 4 + 1] = slot.y;
50            (*scratch)[index * 4 + 2] = slot.w;
51            (*scratch)[index * 4 + 3] = slot.h;
52        }
53    }
54    written as u32
55}
56
57fn settings(gap: f64, ratio: f64, column: f64, padding: f64) -> Settings {
58    Settings {
59        gap,
60        ratio,
61        column,
62        padding,
63        // The preview drives the arrangements it has controls for; dwindle
64        // takes its split from the default until the page grows a slider.
65        ..Settings::default()
66    }
67}
68
69#[expect(unsafe_code, reason = "exporting a C symbol")]
70#[unsafe(no_mangle)]
71pub extern "C" fn solium_layout_master_stack(
72    count: u32,
73    x: f64,
74    y: f64,
75    w: f64,
76    h: f64,
77    gap: f64,
78    ratio: f64,
79) -> u32 {
80    let slots = crate::master_stack(
81        count as usize,
82        Rect::new(x, y, w, h),
83        settings(gap, ratio, 0.44, 24.0),
84    );
85    publish(&slots)
86}
87
88#[expect(unsafe_code, reason = "exporting a C symbol")]
89#[unsafe(no_mangle)]
90pub extern "C" fn solium_layout_scrolling(
91    count: u32,
92    x: f64,
93    y: f64,
94    w: f64,
95    h: f64,
96    gap: f64,
97    column: f64,
98    offset: f64,
99) -> u32 {
100    let slots = crate::scrolling(
101        count as usize,
102        Rect::new(x, y, w, h),
103        settings(gap, 0.6, column, 24.0),
104        offset,
105    );
106    publish(&slots)
107}
108
109/// The grid, given each window's own rectangle in the scratch buffer.
110///
111/// Written in, read out of the same place: the caller fills the scratch with
112/// `count` rectangles, calls this, and reads the arrangement back from it.
113#[expect(unsafe_code, reason = "exporting a C symbol and reading its scratch")]
114#[unsafe(no_mangle)]
115pub extern "C" fn solium_layout_grid(
116    count: u32,
117    x: f64,
118    y: f64,
119    w: f64,
120    h: f64,
121    padding: f64,
122) -> u32 {
123    let count = (count as usize).min(CAPACITY);
124    let scratch = &raw const SCRATCH;
125    let sizes: Vec<Rect> = (0..count)
126        .map(|index| {
127            // SAFETY: as in `publish`, and `count` is clamped to the buffer.
128            unsafe {
129                Rect::new(
130                    (*scratch)[index * 4],
131                    (*scratch)[index * 4 + 1],
132                    (*scratch)[index * 4 + 2],
133                    (*scratch)[index * 4 + 3],
134                )
135            }
136        })
137        .collect();
138
139    let slots = crate::grid(
140        &sizes,
141        Rect::new(x, y, w, h),
142        settings(12.0, 0.6, 0.44, padding),
143    );
144    publish(&slots)
145}
146
147#[expect(unsafe_code, reason = "exporting a C symbol")]
148#[unsafe(no_mangle)]
149pub extern "C" fn solium_layout_scroll_to(
150    index: u32,
151    count: u32,
152    x: f64,
153    y: f64,
154    w: f64,
155    h: f64,
156    gap: f64,
157    column: f64,
158    offset: f64,
159) -> f64 {
160    crate::scroll_to(
161        index as usize,
162        count as usize,
163        Rect::new(x, y, w, h),
164        settings(gap, 0.6, column, 24.0),
165        offset,
166    )
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    #[test]
174    #[expect(unsafe_code, reason = "reading the scratch the call just wrote")]
175    fn the_exported_arrangement_matches_the_engine() {
176        let written = solium_layout_master_stack(3, 0.0, 34.0, 1600.0, 866.0, 12.0, 0.6);
177        assert_eq!(written, 3);
178
179        let direct =
180            crate::master_stack(3, Rect::new(0.0, 34.0, 1600.0, 866.0), Settings::default());
181        let scratch = &raw const SCRATCH;
182        for (index, slot) in direct.iter().enumerate() {
183            // SAFETY: single-threaded test, reading what the call above wrote.
184            unsafe {
185                assert!(((*scratch)[index * 4] - slot.x).abs() < f64::EPSILON);
186                assert!(((*scratch)[index * 4 + 2] - slot.w).abs() < f64::EPSILON);
187            }
188        }
189    }
190
191    #[test]
192    fn more_windows_than_the_buffer_holds_are_dropped_not_written_past() {
193        let written = solium_layout_master_stack(500, 0.0, 0.0, 1600.0, 900.0, 12.0, 0.6);
194        assert_eq!(written as usize, CAPACITY);
195    }
196}