Skip to main content

noesis_runtime/
shapes.rs

1//! Build [`Rectangle`], [`Ellipse`], and [`Line`] shapes from Rust and set
2//! their drawing properties without authoring XAML.
3//!
4//! Each handle owns a freshly-created Noesis shape holding a single `+1`
5//! reference released on [`Drop`], the same ownership idiom as
6//! [`crate::brushes`] / [`crate::transforms`]. A shape *is* a
7//! [`FrameworkElement`](crate::view::FrameworkElement), so once built you can
8//! hand its [`raw`](Rectangle::raw) pointer to the element tree (e.g. as a
9//! `Panel` child) and let Noesis take its own reference, after which the Rust
10//! handle may be dropped.
11//!
12//! `Fill` and `Stroke` accept any [`Brush`] handle from [`crate::brushes`];
13//! Noesis takes its own reference to the brush, so the handle may be dropped
14//! afterwards.
15//!
16//! Every setter has a matching getter that re-reads from the live Noesis object
17//! rather than echoing a Rust-side cache.
18//!
19//! # SDK scope
20//!
21//! Noesis ships only `Rectangle`, `Ellipse`, `Line`, and `Path` as shape
22//! elements; there is **no** `Polygon`/`Polyline`. Build a polygon or polyline
23//! as a `PathGeometry`/`StreamGeometry` hosted in a `Path`.
24
25use core::ptr::NonNull;
26use std::ffi::{CStr, CString, c_void};
27
28use crate::brushes::Brush;
29use crate::ffi::{
30    noesis_base_component_add_reference, noesis_base_component_release, noesis_ellipse_create,
31    noesis_line_create, noesis_line_get, noesis_line_set, noesis_rectangle_create,
32    noesis_rectangle_get_radius_x, noesis_rectangle_get_radius_y, noesis_rectangle_set_radius_x,
33    noesis_rectangle_set_radius_y, noesis_shape_get_fill, noesis_shape_get_height,
34    noesis_shape_get_stretch, noesis_shape_get_stroke, noesis_shape_get_stroke_dash_array,
35    noesis_shape_get_stroke_dash_cap, noesis_shape_get_stroke_dash_offset,
36    noesis_shape_get_stroke_end_line_cap, noesis_shape_get_stroke_line_join,
37    noesis_shape_get_stroke_miter_limit, noesis_shape_get_stroke_start_line_cap,
38    noesis_shape_get_stroke_thickness, noesis_shape_get_trim_end, noesis_shape_get_trim_offset,
39    noesis_shape_get_trim_start, noesis_shape_get_width, noesis_shape_set_fill,
40    noesis_shape_set_height, noesis_shape_set_stretch, noesis_shape_set_stroke,
41    noesis_shape_set_stroke_dash_array, noesis_shape_set_stroke_dash_cap,
42    noesis_shape_set_stroke_dash_offset, noesis_shape_set_stroke_end_line_cap,
43    noesis_shape_set_stroke_line_join, noesis_shape_set_stroke_miter_limit,
44    noesis_shape_set_stroke_start_line_cap, noesis_shape_set_stroke_thickness,
45    noesis_shape_set_trim_end, noesis_shape_set_trim_offset, noesis_shape_set_trim_start,
46    noesis_shape_set_width,
47};
48
49/// How the ends of a dash (or a line) are drawn. Ordinals mirror
50/// `Noesis::PenLineCap`.
51#[derive(Copy, Clone, Debug, PartialEq, Eq)]
52#[repr(i32)]
53#[non_exhaustive]
54pub enum PenLineCap {
55    /// A cap that does not extend past the last point of the line.
56    Flat = 0,
57    /// A rectangle half the line thickness long.
58    Square = 1,
59    /// A semicircle with diameter equal to the line thickness.
60    Round = 2,
61    /// An isosceles right triangle.
62    Triangle = 3,
63}
64
65/// How the vertices of a shape are joined. Ordinals mirror
66/// `Noesis::PenLineJoin`.
67#[derive(Copy, Clone, Debug, PartialEq, Eq)]
68#[repr(i32)]
69#[non_exhaustive]
70pub enum PenLineJoin {
71    /// Regular angular (mitered) vertices.
72    Miter = 0,
73    /// Beveled vertices.
74    Bevel = 1,
75    /// Rounded vertices.
76    Round = 2,
77}
78
79/// How a shape fills its allocated space. Re-exported from [`crate::brushes`]
80/// so the crate has a single `Stretch` type (ordinals mirror `Noesis::Stretch`).
81pub use crate::brushes::Stretch;
82
83impl PenLineCap {
84    fn from_ordinal(v: i32) -> Option<Self> {
85        match v {
86            0 => Some(Self::Flat),
87            1 => Some(Self::Square),
88            2 => Some(Self::Round),
89            3 => Some(Self::Triangle),
90            _ => None,
91        }
92    }
93}
94
95impl PenLineJoin {
96    fn from_ordinal(v: i32) -> Option<Self> {
97        match v {
98            0 => Some(Self::Miter),
99            1 => Some(Self::Bevel),
100            2 => Some(Self::Round),
101            _ => None,
102        }
103    }
104}
105
106/// A handle to a Noesis `Shape` (the base class of [`Rectangle`], [`Ellipse`],
107/// and [`Line`]). The trait carries every property defined on `Shape` plus the
108/// inherited `FrameworkElement` `Width`/`Height`, so all three concrete shapes
109/// share one implementation.
110///
111/// Getters re-read from the live Noesis object.
112pub trait Shape {
113    /// Borrowed `Noesis::Shape*` (also a `FrameworkElement*` / `BaseComponent*`),
114    /// valid for `self`'s lifetime. Used by the shared property methods and by
115    /// callers handing the shape to other Noesis APIs (e.g. tree insertion).
116    fn shape_raw(&self) -> *mut c_void;
117
118    /// View this shape as an owning [`FrameworkElement`](crate::view::FrameworkElement)
119    /// handle (a fresh `+1`) so it can be handed to element-tree APIs that take a
120    /// `&FrameworkElement` — e.g.
121    /// [`FrameworkElement::set_content`](crate::view::FrameworkElement::set_content)
122    /// (a `ContentControl`'s `Content`) or
123    /// [`FrameworkElement::set_decorator_child`](crate::view::FrameworkElement::set_decorator_child)
124    /// (a `Border`/`Decorator`'s `Child`). Dropping the returned handle releases
125    /// only its own reference and does not affect `self`.
126    #[must_use]
127    fn as_element(&self) -> crate::view::FrameworkElement {
128        // SAFETY: shape_raw() is a live FrameworkElement*/BaseComponent*; AddRef
129        // hands back an independent +1 the returned handle owns and releases on
130        // drop.
131        let p = unsafe { noesis_base_component_add_reference(self.shape_raw()) };
132        let ptr = NonNull::new(p).expect("add_reference returned null on a live shape");
133        // SAFETY: `ptr` carries the +1 we just took ownership of.
134        unsafe { crate::view::FrameworkElement::from_owned(ptr) }
135    }
136
137    /// Set the element's explicit `Width` (a `FrameworkElement` DP; `NaN` ==
138    /// "auto").
139    fn set_width(&mut self, width: f32) {
140        // SAFETY: shape_raw() is a live Shape*/FrameworkElement*.
141        unsafe { noesis_shape_set_width(self.shape_raw(), width) };
142    }
143
144    /// Read the element's explicit `Width` back from the live object.
145    #[must_use]
146    fn width(&self) -> f32 {
147        let mut out = 0.0f32;
148        // SAFETY: shape_raw() is live; `out` is a valid f32 slot.
149        unsafe { noesis_shape_get_width(self.shape_raw(), &mut out) };
150        out
151    }
152
153    /// Set the element's explicit `Height` (`NaN` == "auto").
154    fn set_height(&mut self, height: f32) {
155        // SAFETY: shape_raw() is live.
156        unsafe { noesis_shape_set_height(self.shape_raw(), height) };
157    }
158
159    /// Read the element's explicit `Height` back from the live object.
160    #[must_use]
161    fn height(&self) -> f32 {
162        let mut out = 0.0f32;
163        // SAFETY: shape_raw() is live; `out` is a valid f32 slot.
164        unsafe { noesis_shape_get_height(self.shape_raw(), &mut out) };
165        out
166    }
167
168    /// Paint the shape's interior with `brush` (any [`Brush`] handle). Noesis
169    /// takes its own reference, so the brush handle may be dropped afterwards.
170    fn set_fill<B: Brush>(&mut self, brush: &B) {
171        // SAFETY: shape_raw() is live; brush_raw() is a live Brush* borrowed for
172        // the call; Noesis stores its own reference.
173        unsafe { noesis_shape_set_fill(self.shape_raw(), brush.brush_raw()) };
174    }
175
176    /// Clear the shape's `Fill`.
177    fn clear_fill(&mut self) {
178        // SAFETY: shape_raw() is live; a null brush clears the property.
179        unsafe { noesis_shape_set_fill(self.shape_raw(), core::ptr::null_mut()) };
180    }
181
182    /// Borrowed `Brush*` currently set as `Fill`, or null if unset. Returned
183    /// without a `+1`; use it only for identity checks against a brush handle's
184    /// [`raw`](crate::brushes::SolidColorBrush::raw).
185    #[must_use]
186    fn fill_raw(&self) -> *mut c_void {
187        // SAFETY: shape_raw() is live; the returned pointer is borrowed.
188        unsafe { noesis_shape_get_fill(self.shape_raw()) }
189    }
190
191    /// Paint the shape's outline with `brush`.
192    fn set_stroke<B: Brush>(&mut self, brush: &B) {
193        // SAFETY: shape_raw() is live; brush_raw() is a live Brush* for the call.
194        unsafe { noesis_shape_set_stroke(self.shape_raw(), brush.brush_raw()) };
195    }
196
197    /// Clear the shape's `Stroke`.
198    fn clear_stroke(&mut self) {
199        // SAFETY: shape_raw() is live; a null brush clears the property.
200        unsafe { noesis_shape_set_stroke(self.shape_raw(), core::ptr::null_mut()) };
201    }
202
203    /// Borrowed `Brush*` currently set as `Stroke`, or null if unset.
204    #[must_use]
205    fn stroke_raw(&self) -> *mut c_void {
206        // SAFETY: shape_raw() is live; the returned pointer is borrowed.
207        unsafe { noesis_shape_get_stroke(self.shape_raw()) }
208    }
209
210    /// Set the outline width.
211    fn set_stroke_thickness(&mut self, value: f32) {
212        // SAFETY: shape_raw() is live.
213        unsafe { noesis_shape_set_stroke_thickness(self.shape_raw(), value) };
214    }
215
216    /// Read the outline width back from the live object.
217    #[must_use]
218    fn stroke_thickness(&self) -> f32 {
219        let mut out = 0.0f32;
220        // SAFETY: shape_raw() is live; `out` is valid.
221        unsafe { noesis_shape_get_stroke_thickness(self.shape_raw(), &mut out) };
222        out
223    }
224
225    /// Set the miter-length limit (ratio to half the `StrokeThickness`).
226    fn set_stroke_miter_limit(&mut self, value: f32) {
227        // SAFETY: shape_raw() is live.
228        unsafe { noesis_shape_set_stroke_miter_limit(self.shape_raw(), value) };
229    }
230
231    /// Read the miter limit back from the live object.
232    #[must_use]
233    fn stroke_miter_limit(&self) -> f32 {
234        let mut out = 0.0f32;
235        // SAFETY: shape_raw() is live; `out` is valid.
236        unsafe { noesis_shape_get_stroke_miter_limit(self.shape_raw(), &mut out) };
237        out
238    }
239
240    /// Set the distance into the dash pattern at which a dash begins.
241    fn set_stroke_dash_offset(&mut self, value: f32) {
242        // SAFETY: shape_raw() is live.
243        unsafe { noesis_shape_set_stroke_dash_offset(self.shape_raw(), value) };
244    }
245
246    /// Read the dash offset back from the live object.
247    #[must_use]
248    fn stroke_dash_offset(&self) -> f32 {
249        let mut out = 0.0f32;
250        // SAFETY: shape_raw() is live; `out` is valid.
251        unsafe { noesis_shape_get_stroke_dash_offset(self.shape_raw(), &mut out) };
252        out
253    }
254
255    /// Set the amount to trim from the start of the geometry path (`0..=1`).
256    fn set_trim_start(&mut self, value: f32) {
257        // SAFETY: shape_raw() is live.
258        unsafe { noesis_shape_set_trim_start(self.shape_raw(), value) };
259    }
260
261    /// Read the trim-start back from the live object.
262    #[must_use]
263    fn trim_start(&self) -> f32 {
264        let mut out = 0.0f32;
265        // SAFETY: shape_raw() is live; `out` is valid.
266        unsafe { noesis_shape_get_trim_start(self.shape_raw(), &mut out) };
267        out
268    }
269
270    /// Set the amount to trim from the end of the geometry path (`0..=1`).
271    fn set_trim_end(&mut self, value: f32) {
272        // SAFETY: shape_raw() is live.
273        unsafe { noesis_shape_set_trim_end(self.shape_raw(), value) };
274    }
275
276    /// Read the trim-end back from the live object.
277    #[must_use]
278    fn trim_end(&self) -> f32 {
279        let mut out = 0.0f32;
280        // SAFETY: shape_raw() is live; `out` is valid.
281        unsafe { noesis_shape_get_trim_end(self.shape_raw(), &mut out) };
282        out
283    }
284
285    /// Set the amount to offset trimming the geometry path.
286    fn set_trim_offset(&mut self, value: f32) {
287        // SAFETY: shape_raw() is live.
288        unsafe { noesis_shape_set_trim_offset(self.shape_raw(), value) };
289    }
290
291    /// Read the trim-offset back from the live object.
292    #[must_use]
293    fn trim_offset(&self) -> f32 {
294        let mut out = 0.0f32;
295        // SAFETY: shape_raw() is live; `out` is valid.
296        unsafe { noesis_shape_get_trim_offset(self.shape_raw(), &mut out) };
297        out
298    }
299
300    /// Set the cap drawn at the ends of each dash.
301    fn set_stroke_dash_cap(&mut self, cap: PenLineCap) {
302        // SAFETY: shape_raw() is live.
303        unsafe { noesis_shape_set_stroke_dash_cap(self.shape_raw(), cap as i32) };
304    }
305
306    /// Read the dash cap back from the live object.
307    #[must_use]
308    fn stroke_dash_cap(&self) -> Option<PenLineCap> {
309        // SAFETY: shape_raw() is live.
310        PenLineCap::from_ordinal(unsafe { noesis_shape_get_stroke_dash_cap(self.shape_raw()) })
311    }
312
313    /// Set the cap drawn at the start of the stroke.
314    fn set_stroke_start_line_cap(&mut self, cap: PenLineCap) {
315        // SAFETY: shape_raw() is live.
316        unsafe { noesis_shape_set_stroke_start_line_cap(self.shape_raw(), cap as i32) };
317    }
318
319    /// Read the start line cap back from the live object.
320    #[must_use]
321    fn stroke_start_line_cap(&self) -> Option<PenLineCap> {
322        // SAFETY: shape_raw() is live.
323        PenLineCap::from_ordinal(unsafe {
324            noesis_shape_get_stroke_start_line_cap(self.shape_raw())
325        })
326    }
327
328    /// Set the cap drawn at the end of the stroke.
329    fn set_stroke_end_line_cap(&mut self, cap: PenLineCap) {
330        // SAFETY: shape_raw() is live.
331        unsafe { noesis_shape_set_stroke_end_line_cap(self.shape_raw(), cap as i32) };
332    }
333
334    /// Read the end line cap back from the live object.
335    #[must_use]
336    fn stroke_end_line_cap(&self) -> Option<PenLineCap> {
337        // SAFETY: shape_raw() is live.
338        PenLineCap::from_ordinal(unsafe { noesis_shape_get_stroke_end_line_cap(self.shape_raw()) })
339    }
340
341    /// Set the join used at the vertices of the stroke.
342    fn set_stroke_line_join(&mut self, join: PenLineJoin) {
343        // SAFETY: shape_raw() is live.
344        unsafe { noesis_shape_set_stroke_line_join(self.shape_raw(), join as i32) };
345    }
346
347    /// Read the line join back from the live object.
348    #[must_use]
349    fn stroke_line_join(&self) -> Option<PenLineJoin> {
350        // SAFETY: shape_raw() is live.
351        PenLineJoin::from_ordinal(unsafe { noesis_shape_get_stroke_line_join(self.shape_raw()) })
352    }
353
354    /// Set how the shape stretches to fill its allocated space.
355    fn set_stretch(&mut self, stretch: Stretch) {
356        // SAFETY: shape_raw() is live.
357        unsafe { noesis_shape_set_stretch(self.shape_raw(), stretch as i32) };
358    }
359
360    /// Read the stretch mode back from the live object.
361    #[must_use]
362    fn stretch(&self) -> Option<Stretch> {
363        // SAFETY: shape_raw() is live.
364        Stretch::from_ordinal(unsafe { noesis_shape_get_stretch(self.shape_raw()) })
365    }
366
367    /// Set the dash pattern. Noesis exposes this as a space-separated string of
368    /// dash/gap lengths (e.g. `"2 1 3"`).
369    ///
370    /// # Panics
371    ///
372    /// Panics if `dashes` contains an interior NUL byte.
373    fn set_stroke_dash_array(&mut self, dashes: &str) {
374        let c = CString::new(dashes).expect("dash array contained NUL");
375        // SAFETY: shape_raw() is live; `c` outlives the call and the C side
376        // copies it into the Noesis object.
377        unsafe { noesis_shape_set_stroke_dash_array(self.shape_raw(), c.as_ptr()) };
378    }
379
380    /// Read the dash pattern back from the live object as an owned `String`
381    /// (empty if unset).
382    #[must_use]
383    fn stroke_dash_array(&self) -> String {
384        // SAFETY: shape_raw() is live; the returned pointer is owned by the
385        // Noesis object and valid until the next mutation, so we copy immediately.
386        let p = unsafe { noesis_shape_get_stroke_dash_array(self.shape_raw()) };
387        if p.is_null() {
388            String::new()
389        } else {
390            // SAFETY: `p` is a NUL-terminated C string from Noesis.
391            unsafe { CStr::from_ptr(p) }.to_string_lossy().into_owned()
392        }
393    }
394}
395
396macro_rules! shape_handle {
397    ($name:ident, $create:ident, $doc:literal) => {
398        #[doc = $doc]
399        pub struct $name {
400            ptr: NonNull<c_void>,
401        }
402
403        // SAFETY: Send-only (NOT Sync); see the crate-level "Thread affinity" docs.
404        unsafe impl Send for $name {}
405
406        impl $name {
407            /// Create the shape with default property values.
408            ///
409            /// # Panics
410            ///
411            /// Panics if Noesis fails to allocate the shape (not expected after
412            /// [`crate::init`]).
413            #[must_use]
414            pub fn new() -> Self {
415                // SAFETY: a plain component-create call; returns a +1 ref we own.
416                let ptr = unsafe { $create() };
417                Self {
418                    ptr: NonNull::new(ptr).expect(concat!(stringify!($create), " returned null")),
419                }
420            }
421
422            /// Raw `Noesis::Shape*` (also a `FrameworkElement*` /
423            /// `BaseComponent*`). Borrowed for the lifetime of `self`; hand it to
424            /// other Noesis APIs (e.g. inserting the shape into an element tree).
425            #[must_use]
426            pub fn raw(&self) -> *mut c_void {
427                self.ptr.as_ptr()
428            }
429        }
430
431        impl Default for $name {
432            fn default() -> Self {
433                Self::new()
434            }
435        }
436
437        impl Shape for $name {
438            fn shape_raw(&self) -> *mut c_void {
439                self.ptr.as_ptr()
440            }
441        }
442
443        impl Drop for $name {
444            fn drop(&mut self) {
445                // SAFETY: produced by a `*_create` entrypoint with a +1 ref we
446                // own; released exactly once here.
447                unsafe { noesis_base_component_release(self.ptr.as_ptr()) }
448            }
449        }
450    };
451}
452
453shape_handle!(
454    Rectangle,
455    noesis_rectangle_create,
456    "A `Rectangle` shape. Adds rounded-corner radii on top of the shared\n\
457     [`Shape`] surface; its size comes from the inherited\n\
458     [`Shape::set_width`]/[`Shape::set_height`]."
459);
460shape_handle!(
461    Ellipse,
462    noesis_ellipse_create,
463    "An `Ellipse` shape. Carries only the shared [`Shape`] surface; its size\n\
464     comes from the inherited [`Shape::set_width`]/[`Shape::set_height`]."
465);
466shape_handle!(
467    Line,
468    noesis_line_create,
469    "A `Line` shape defined by its two endpoints `(X1, Y1)`-`(X2, Y2)` in\n\
470     addition to the shared [`Shape`] surface."
471);
472
473impl Rectangle {
474    /// Set the x-axis corner radius.
475    pub fn set_radius_x(&mut self, value: f32) {
476        // SAFETY: self.ptr is a live Rectangle*.
477        unsafe { noesis_rectangle_set_radius_x(self.ptr.as_ptr(), value) };
478    }
479
480    /// Read the x-axis corner radius back from the live object.
481    #[must_use]
482    pub fn radius_x(&self) -> f32 {
483        let mut out = 0.0f32;
484        // SAFETY: self.ptr is live; `out` is valid.
485        unsafe { noesis_rectangle_get_radius_x(self.ptr.as_ptr(), &mut out) };
486        out
487    }
488
489    /// Set the y-axis corner radius.
490    pub fn set_radius_y(&mut self, value: f32) {
491        // SAFETY: self.ptr is a live Rectangle*.
492        unsafe { noesis_rectangle_set_radius_y(self.ptr.as_ptr(), value) };
493    }
494
495    /// Read the y-axis corner radius back from the live object.
496    #[must_use]
497    pub fn radius_y(&self) -> f32 {
498        let mut out = 0.0f32;
499        // SAFETY: self.ptr is live; `out` is valid.
500        unsafe { noesis_rectangle_get_radius_y(self.ptr.as_ptr(), &mut out) };
501        out
502    }
503}
504
505impl Line {
506    /// Set both endpoints at once: `(x1, y1)` start and `(x2, y2)` end.
507    pub fn set_points(&mut self, x1: f32, y1: f32, x2: f32, y2: f32) {
508        // SAFETY: self.ptr is a live Line*.
509        unsafe { noesis_line_set(self.ptr.as_ptr(), x1, y1, x2, y2) };
510    }
511
512    /// Read both endpoints back from the live object as `[x1, y1, x2, y2]`.
513    #[must_use]
514    pub fn points(&self) -> [f32; 4] {
515        let mut out = [0.0f32; 4];
516        // SAFETY: self.ptr is live; `out` is a 4-float buffer.
517        unsafe { noesis_line_get(self.ptr.as_ptr(), out.as_mut_ptr()) };
518        out
519    }
520}