clay_layout/
lib.rs

1#![cfg_attr(not(feature = "std"), no_std)]
2
3pub mod bindings;
4pub mod color;
5pub mod elements;
6pub mod errors;
7pub mod id;
8pub mod layout;
9pub mod math;
10pub mod render_commands;
11pub mod text;
12
13mod mem;
14pub mod renderers;
15
16use core::marker::PhantomData;
17
18pub use crate::bindings::*;
19use errors::Error;
20use id::Id;
21use math::{BoundingBox, Dimensions, Vector2};
22use render_commands::RenderCommand;
23
24pub use color::Color;
25
26#[cfg(feature = "std")]
27use text::TextConfig;
28
29use text::TextElementConfig;
30#[derive(Copy, Clone)]
31pub struct Declaration<'render, ImageElementData: 'render, CustomElementData: 'render> {
32    inner: Clay_ElementDeclaration,
33    _phantom: PhantomData<(&'render CustomElementData, &'render ImageElementData)>,
34}
35
36impl<'render, ImageElementData: 'render, CustomElementData: 'render>
37    Declaration<'render, ImageElementData, CustomElementData>
38{
39    #[inline]
40    pub fn new() -> Self {
41        crate::mem::zeroed_init()
42    }
43
44    #[inline]
45    pub fn background_color(&mut self, color: Color) -> &mut Self {
46        self.inner.backgroundColor = color.into();
47        self
48    }
49
50    /// Sets aspect ratio for image elements.
51    #[inline]
52    pub fn aspect_ratio(&mut self, aspect_ratio: f32) -> &mut Self {
53        self.inner.aspectRatio.aspectRatio = aspect_ratio;
54        self
55    }
56
57    #[inline]
58    pub fn clip(&mut self, horizontal: bool, vertical: bool, child_offset: Vector2) -> &mut Self {
59        self.inner.clip.horizontal = horizontal;
60        self.inner.clip.vertical = vertical;
61        self.inner.clip.childOffset = child_offset.into();
62        self
63    }
64
65    #[inline]
66    pub fn id(&mut self, id: Id) -> &mut Self {
67        self.inner.id = id.id;
68        self
69    }
70
71    #[inline]
72    pub fn custom_element(&mut self, data: &'render CustomElementData) -> &mut Self {
73        self.inner.custom.customData = data as *const CustomElementData as _;
74        self
75    }
76
77    #[inline]
78    pub fn layout(
79        &mut self,
80    ) -> layout::LayoutBuilder<'_, 'render, ImageElementData, CustomElementData> {
81        layout::LayoutBuilder::new(self)
82    }
83
84    #[inline]
85    pub fn image(
86        &mut self,
87    ) -> elements::ImageBuilder<'_, 'render, ImageElementData, CustomElementData> {
88        elements::ImageBuilder::new(self)
89    }
90
91    #[inline]
92    pub fn floating(
93        &mut self,
94    ) -> elements::FloatingBuilder<'_, 'render, ImageElementData, CustomElementData> {
95        elements::FloatingBuilder::new(self)
96    }
97
98    #[inline]
99    pub fn border(
100        &mut self,
101    ) -> elements::BorderBuilder<'_, 'render, ImageElementData, CustomElementData> {
102        elements::BorderBuilder::new(self)
103    }
104
105    #[inline]
106    pub fn corner_radius(
107        &mut self,
108    ) -> elements::CornerRadiusBuilder<'_, 'render, ImageElementData, CustomElementData> {
109        elements::CornerRadiusBuilder::new(self)
110    }
111}
112
113impl<ImageElementData, CustomElementData> Default
114    for Declaration<'_, ImageElementData, CustomElementData>
115{
116    fn default() -> Self {
117        Self::new()
118    }
119}
120
121#[cfg(feature = "std")]
122unsafe extern "C" fn measure_text_trampoline_user_data<'a, F, T>(
123    text_slice: Clay_StringSlice,
124    config: *mut Clay_TextElementConfig,
125    user_data: *mut core::ffi::c_void,
126) -> Clay_Dimensions
127where
128    F: Fn(&str, &TextConfig, &'a mut T) -> Dimensions + 'a,
129    T: 'a,
130{
131    let text = core::str::from_utf8_unchecked(core::slice::from_raw_parts(
132        text_slice.chars as *const u8,
133        text_slice.length as _,
134    ));
135
136    let closure_and_data: &mut (F, T) = &mut *(user_data as *mut (F, T));
137    let text_config = TextConfig::from(*config);
138    let (callback, data) = closure_and_data;
139    callback(text, &text_config, data).into()
140}
141
142#[cfg(feature = "std")]
143unsafe extern "C" fn measure_text_trampoline<'a, F>(
144    text_slice: Clay_StringSlice,
145    config: *mut Clay_TextElementConfig,
146    user_data: *mut core::ffi::c_void,
147) -> Clay_Dimensions
148where
149    F: Fn(&str, &TextConfig) -> Dimensions + 'a,
150{
151    let text = core::str::from_utf8_unchecked(core::slice::from_raw_parts(
152        text_slice.chars as *const u8,
153        text_slice.length as _,
154    ));
155
156    let tuple = &*(user_data as *const (F, usize));
157    let text_config = TextConfig::from(*config);
158    (tuple.0)(text, &text_config).into()
159}
160
161unsafe extern "C" fn error_handler(error_data: Clay_ErrorData) {
162    let error: Error = error_data.into();
163    panic!("Clay Error: (type: {:?}) {}", error.type_, error.text);
164}
165
166#[allow(dead_code)]
167pub struct Clay {
168    /// Memory used internally by clay
169    #[cfg(feature = "std")]
170    _memory: Vec<u8>,
171    context: *mut Clay_Context,
172    /// Memory used internally by clay. The caller is responsible for managing this memory in
173    /// no_std case.
174    #[cfg(not(feature = "std"))]
175    _memory: *const core::ffi::c_void,
176    /// Stores the raw pointer to the callback data for later cleanup
177    text_measure_callback: Option<*const core::ffi::c_void>,
178}
179
180pub struct ClayLayoutScope<'clay, 'render, ImageElementData, CustomElementData> {
181    clay: &'clay mut Clay,
182    _phantom: core::marker::PhantomData<(&'render ImageElementData, &'render CustomElementData)>,
183    dropped: bool,
184}
185
186impl<'render, 'clay: 'render, ImageElementData: 'render, CustomElementData: 'render>
187    ClayLayoutScope<'clay, 'render, ImageElementData, CustomElementData>
188{
189    /// Create an element, passing its config and a function to add childrens
190    pub fn with<
191        F: FnOnce(&mut ClayLayoutScope<'clay, 'render, ImageElementData, CustomElementData>),
192    >(
193        &mut self,
194        declaration: &Declaration<'render, ImageElementData, CustomElementData>,
195        f: F,
196    ) {
197        unsafe {
198            Clay_SetCurrentContext(self.clay.context);
199            Clay__OpenElement();
200            Clay__ConfigureOpenElement(declaration.inner);
201        }
202
203        f(self);
204
205        unsafe {
206            Clay__CloseElement();
207        }
208    }
209
210    pub fn with_styling<
211        G: FnOnce(
212            &ClayLayoutScope<'clay, 'render, ImageElementData, CustomElementData>,
213        ) -> Declaration<'render, ImageElementData, CustomElementData>,
214        F: FnOnce(&ClayLayoutScope<'clay, 'render, ImageElementData, CustomElementData>),
215    >(
216        &self,
217        g: G,
218        f: F,
219    ) {
220        unsafe {
221            Clay_SetCurrentContext(self.clay.context);
222            Clay__OpenElement();
223        }
224
225        let declaration = g(self);
226
227        unsafe {
228            Clay__ConfigureOpenElement(declaration.inner);
229        }
230
231        f(self);
232
233        unsafe {
234            Clay__CloseElement();
235        }
236    }
237
238    pub fn end(
239        &mut self,
240    ) -> impl Iterator<Item = RenderCommand<'render, ImageElementData, CustomElementData>> {
241        let array = unsafe { Clay_EndLayout() };
242        self.dropped = true;
243        let slice = unsafe { core::slice::from_raw_parts(array.internalArray, array.length as _) };
244        slice
245            .iter()
246            .map(|command| unsafe { RenderCommand::from_clay_render_command(*command) })
247    }
248
249    /// Generates a unique ID based on the given `label`.
250    ///
251    /// This ID is global and must be unique across the entire scope.
252    #[inline]
253    pub fn id(&self, label: &'render str) -> id::Id {
254        id::Id::new(label)
255    }
256
257    /// Generates a unique indexed ID based on the given `label` and `index`.
258    ///
259    /// This is useful when multiple elements share the same label but need distinct IDs.
260    #[inline]
261    pub fn id_index(&self, label: &'render str, index: u32) -> id::Id {
262        id::Id::new_index(label, index)
263    }
264
265    /// Generates a locally unique ID based on the given `label`.
266    ///
267    /// The ID is unique within a specific local scope but not globally.
268    #[inline]
269    pub fn id_local(&self, label: &'render str) -> id::Id {
270        id::Id::new_index_local(label, 0)
271    }
272
273    /// Generates a locally unique indexed ID based on the given `label` and `index`.
274    ///
275    /// This is useful for differentiating elements within a local scope while keeping their labels consistent.
276    #[inline]
277    pub fn id_index_local(&self, label: &'render str, index: u32) -> id::Id {
278        id::Id::new_index_local(label, index)
279    }
280
281    /// Adds a text element to the current open element or to the root layout
282    pub fn text(&self, text: &'render str, config: TextElementConfig) {
283        unsafe { Clay__OpenTextElement(text.into(), config.into()) };
284    }
285
286    pub fn hovered(&self) -> bool {
287        unsafe { Clay_Hovered() }
288    }
289
290    pub fn pointer_over(&self, cfg: Id) -> bool {
291        unsafe { Clay_PointerOver(cfg.id) }
292    }
293
294    pub fn scroll_container_data(&self, id: Id) -> Option<Clay_ScrollContainerData> {
295        self.clay.scroll_container_data(id)
296    }
297    pub fn bounding_box(&self, id: Id) -> Option<BoundingBox> {
298        self.clay.bounding_box(id)
299    }
300
301    pub fn scroll_offset(&self) -> Vector2 {
302        unsafe { Clay_GetScrollOffset().into() }
303    }
304}
305
306impl<ImageElementData, CustomElementData> Drop
307    for ClayLayoutScope<'_, '_, ImageElementData, CustomElementData>
308{
309    fn drop(&mut self) {
310        if !self.dropped {
311            unsafe {
312                Clay_EndLayout();
313            }
314        }
315    }
316}
317
318impl Clay {
319    pub fn begin<'render, ImageElementData: 'render, CustomElementData: 'render>(
320        &mut self,
321    ) -> ClayLayoutScope<'_, 'render, ImageElementData, CustomElementData> {
322        unsafe { Clay_BeginLayout() };
323        ClayLayoutScope {
324            clay: self,
325            _phantom: core::marker::PhantomData,
326            dropped: false,
327        }
328    }
329
330    #[cfg(feature = "std")]
331    pub fn new(dimensions: Dimensions) -> Self {
332        let memory_size = Self::required_memory_size();
333        let memory = vec![0; memory_size];
334        let context;
335
336        unsafe {
337            let arena =
338                Clay_CreateArenaWithCapacityAndMemory(memory_size as _, memory.as_ptr() as _);
339
340            context = Clay_Initialize(
341                arena,
342                dimensions.into(),
343                Clay_ErrorHandler {
344                    errorHandlerFunction: Some(error_handler),
345                    userData: std::ptr::null_mut(),
346                },
347            );
348        }
349
350        Self {
351            _memory: memory,
352            context,
353            text_measure_callback: None,
354        }
355    }
356
357    #[cfg(not(feature = "std"))]
358    pub unsafe fn new_with_memory(dimensions: Dimensions, memory: *mut core::ffi::c_void) -> Self {
359        let memory_size = Self::required_memory_size();
360        let arena = Clay_CreateArenaWithCapacityAndMemory(memory_size as _, memory);
361
362        let context = Clay_Initialize(
363            arena,
364            dimensions.into(),
365            Clay_ErrorHandler {
366                errorHandlerFunction: Some(error_handler),
367                userData: core::ptr::null_mut(),
368            },
369        );
370
371        Self {
372            _memory: memory,
373            context,
374            text_measure_callback: None,
375        }
376    }
377
378    /// Wrapper for `Clay_MinMemorySize`, returns the minimum required memory by clay
379    pub fn required_memory_size() -> usize {
380        unsafe { Clay_MinMemorySize() as usize }
381    }
382
383    /// Set the callback for text measurement with user data
384    #[cfg(feature = "std")]
385    pub fn set_measure_text_function_user_data<'clay, F, T>(
386        &'clay mut self,
387        userdata: T,
388        callback: F,
389    ) where
390        F: Fn(&str, &TextConfig, &'clay mut T) -> Dimensions + 'static,
391        T: 'clay,
392    {
393        // Box the callback and userdata together
394        let boxed = Box::new((callback, userdata));
395
396        // Get a raw pointer to the boxed data
397        let user_data_ptr = Box::into_raw(boxed) as _;
398
399        // Register the callback with the external C function
400        unsafe {
401            Self::set_measure_text_function_unsafe(
402                measure_text_trampoline_user_data::<F, T>,
403                user_data_ptr,
404            );
405        }
406
407        // Store the raw pointer for later cleanup
408        self.text_measure_callback = Some(user_data_ptr as *const core::ffi::c_void);
409    }
410
411    /// Set the callback for text measurement
412    #[cfg(feature = "std")]
413    pub fn set_measure_text_function<F>(&mut self, callback: F)
414    where
415        F: Fn(&str, &TextConfig) -> Dimensions + 'static,
416    {
417        // Box the callback and userdata together
418        // Tuple here is to prevent Rust ZST optimization from breaking getting a raw pointer
419        let boxed = Box::new((callback, 0usize));
420
421        // Get a raw pointer to the boxed data
422        let user_data_ptr = Box::into_raw(boxed) as *mut core::ffi::c_void;
423
424        // Register the callback with the external C function
425        unsafe {
426            Self::set_measure_text_function_unsafe(measure_text_trampoline::<F>, user_data_ptr);
427        }
428
429        // Store the raw pointer for later cleanup
430        self.text_measure_callback = Some(user_data_ptr as *const core::ffi::c_void);
431    }
432
433    /// Set the callback for text measurement with user data.
434    /// # Safety
435    /// This function is unsafe because it sets a callback function without any error checking
436    pub unsafe fn set_measure_text_function_unsafe(
437        callback: unsafe extern "C" fn(
438            Clay_StringSlice,
439            *mut Clay_TextElementConfig,
440            *mut core::ffi::c_void,
441        ) -> Clay_Dimensions,
442        user_data: *mut core::ffi::c_void,
443    ) {
444        Clay_SetMeasureTextFunction(Some(callback), user_data);
445    }
446
447    /// Sets the maximum number of element that clay supports
448    /// **Use only if you know what you are doing or your getting errors from clay**
449    pub fn max_element_count(&mut self, max_element_count: u32) {
450        unsafe {
451            Clay_SetMaxElementCount(max_element_count as _);
452        }
453    }
454    /// Sets the capacity of the cache used for text in the measure text function
455    /// **Use only if you know what you are doing or your getting errors from clay**
456    pub fn max_measure_text_cache_word_count(&self, count: u32) {
457        unsafe {
458            Clay_SetMaxElementCount(count as _);
459        }
460    }
461
462    /// Enables or disables the debug mode of clay
463    pub fn set_debug_mode(&self, enable: bool) {
464        unsafe {
465            Clay_SetDebugModeEnabled(enable);
466        }
467    }
468
469    /// Sets the dimensions of the global layout, use if, for example the window size you render to
470    /// changed
471    pub fn set_layout_dimensions(&self, dimensions: Dimensions) {
472        unsafe {
473            Clay_SetLayoutDimensions(dimensions.into());
474        }
475    }
476    /// Updates the state of the pointer for clay. Used to update scroll containers and for
477    /// interactions functions
478    pub fn pointer_state(&self, position: Vector2, is_down: bool) {
479        unsafe {
480            Clay_SetPointerState(position.into(), is_down);
481        }
482    }
483    pub fn update_scroll_containers(
484        &self,
485        drag_scrolling_enabled: bool,
486        scroll_delta: Vector2,
487        delta_time: f32,
488    ) {
489        unsafe {
490            Clay_UpdateScrollContainers(drag_scrolling_enabled, scroll_delta.into(), delta_time);
491        }
492    }
493
494    /// Returns if the current element you are creating is hovered
495    pub fn hovered(&self) -> bool {
496        unsafe { Clay_Hovered() }
497    }
498
499    pub fn pointer_over(&self, cfg: Id) -> bool {
500        unsafe { Clay_PointerOver(cfg.id) }
501    }
502
503    fn element_data(id: Id) -> Clay_ElementData {
504        unsafe { Clay_GetElementData(id.id) }
505    }
506
507    pub fn bounding_box(&self, id: Id) -> Option<BoundingBox> {
508        let element_data = Self::element_data(id);
509
510        if element_data.found {
511            Some(element_data.boundingBox.into())
512        } else {
513            None
514        }
515    }
516    pub fn scroll_container_data(&self, id: Id) -> Option<Clay_ScrollContainerData> {
517        unsafe {
518            Clay_SetCurrentContext(self.context);
519            let scroll_container_data = Clay_GetScrollContainerData(id.id);
520
521            if scroll_container_data.found {
522                Some(scroll_container_data)
523            } else {
524                None
525            }
526        }
527    }
528}
529
530#[cfg(feature = "std")]
531impl Drop for Clay {
532    fn drop(&mut self) {
533        unsafe {
534            if let Some(ptr) = self.text_measure_callback {
535                let _ = Box::from_raw(ptr as *mut (usize, usize));
536            }
537
538            Clay_SetCurrentContext(core::ptr::null_mut() as _);
539        }
540    }
541}
542
543impl From<&str> for Clay_String {
544    fn from(value: &str) -> Self {
545        Self {
546            // TODO: Can we support &'static str here?
547            isStaticallyAllocated: false,
548            length: value.len() as _,
549            chars: value.as_ptr() as _,
550        }
551    }
552}
553
554impl From<Clay_String> for &str {
555    fn from(value: Clay_String) -> Self {
556        unsafe {
557            core::str::from_utf8_unchecked(core::slice::from_raw_parts(
558                value.chars as *const u8,
559                value.length as _,
560            ))
561        }
562    }
563}
564
565impl From<Clay_StringSlice> for &str {
566    fn from(value: Clay_StringSlice) -> Self {
567        unsafe {
568            core::str::from_utf8_unchecked(core::slice::from_raw_parts(
569                value.chars as *const u8,
570                value.length as _,
571            ))
572        }
573    }
574}
575
576#[cfg(test)]
577mod tests {
578    use super::*;
579    use color::Color;
580    use layout::{Padding, Sizing};
581
582    #[rustfmt::skip]
583    #[test]
584    fn test_begin() {
585        let mut callback_data = 0u32;
586
587        let mut clay = Clay::new(Dimensions::new(800.0, 600.0));
588
589        clay.set_measure_text_function_user_data(&mut callback_data, |text, _config, data| {
590            println!(
591                "set_measure_text_function_user_data {:?} count {:?}",
592                text, data
593            );
594            **data += 1;
595            Dimensions::default()
596        });
597
598        let mut clay = clay.begin::<(), ()>();
599
600        clay.with(&Declaration::new()
601            .id(clay.id("parent_rect"))
602            .layout()
603                .width(Sizing::Fixed(100.0))
604                .height(Sizing::Fixed(100.0))
605                .padding(Padding::all(10))
606                .end()
607            .background_color(Color::rgb(255., 255., 255.)), |clay|
608        {
609            clay.with(&Declaration::new()
610                .layout()
611                    .width(Sizing::Fixed(100.0))
612                    .height(Sizing::Fixed(100.0))
613                    .padding(Padding::all(10))
614                    .end()
615                .background_color(Color::rgb(255., 255., 255.)), |clay| 
616            {
617                clay.with(&Declaration::new()
618                    .id(clay.id("rect_under_rect"))
619                    .layout()
620                        .width(Sizing::Fixed(100.0))
621                        .height(Sizing::Fixed(100.0))
622                        .padding(Padding::all(10))
623                        .end()
624                    .background_color(Color::rgb(255., 255., 255.)), |clay| 
625                    {
626                        clay.text("test", TextConfig::new()
627                            .color(Color::rgb(255., 255., 255.))
628                            .font_size(24)
629                            .end());
630                    },
631                );
632            });
633        });
634
635        clay.with(&Declaration::new()
636            .id(clay.id_index("border_container", 1))
637            .layout()
638                .padding(Padding::all(16))
639                .end()
640            .border()
641                .color(Color::rgb(255., 255., 0.))
642                .all_directions(2)
643                .end()
644            .corner_radius().all(10.0).end(), |clay|
645        {
646            clay.with(&Declaration::new()
647                .id(clay.id("rect_under_border"))
648                .layout()
649                    .width(Sizing::Fixed(50.0))
650                    .height(Sizing::Fixed(50.0))
651                    .end()
652                .background_color(Color::rgb(0., 255., 255.)), |_clay| {},
653            );
654        });
655
656        let items = clay.end();
657
658        for item in items {
659            println!(
660                "id: {}\nbbox: {:?}\nconfig: {:?}",
661                item.id, item.bounding_box, item.config,
662            );
663        }
664    }
665
666    #[rustfmt::skip]
667    #[test]
668    fn test_simple_text_measure() {
669        let mut clay = Clay::new(Dimensions::new(800.0, 600.0));
670
671        clay.set_measure_text_function(|_text, _config| {
672            Dimensions::default()
673        });
674
675        let mut clay = clay.begin::<(), ()>();
676
677        clay.with(&Declaration::new()
678            .id(clay.id("parent_rect"))
679            .layout()
680                .width(Sizing::Fixed(100.0))
681                .height(Sizing::Fixed(100.0))
682                .padding(Padding::all(10))
683                .end()
684            .background_color(Color::rgb(255., 255., 255.)), |clay|
685        {
686            clay.text("test", TextConfig::new()
687                .color(Color::rgb(255., 255., 255.))
688                .font_size(24)
689                .end());
690        });
691
692        let _items = clay.end();
693    }
694}