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}