noesis_runtime 0.12.1

Rust bindings for the Noesis GUI Native SDK: load XAML UI, drive the view and renderer, and write custom controls in Rust. Renderer-agnostic; Bevy integration lives in noesis_bevy.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
//! Immediate-mode drawing via `DrawingContext`.
//!
//! In Noesis 3.2.13 a [`DrawingContext`] has a private constructor (friend
//! `UIElement`) and is delivered ONLY to `UIElement::OnRender`. There is no
//! public `DrawingVisual`/`RenderOpen` and no `Drawing`/`DrawingGroup` object
//! model. So immediate-mode drawing is reachable exactly one way: override
//! `OnRender` on a custom element. This module provides:
//!
//! * [`Pen`]: a code-built `Noesis::Pen` (brush + thickness + line caps /
//!   join), the stroke descriptor several draw calls need. Owning handle with a
//!   `+1` reference released on [`Drop`], like the brushes in [`crate::brushes`].
//!   Read-back getters ([`Pen::thickness`], [`Pen::line_caps`], ...) re-read the
//!   live object.
//! * [`DrawingContext::draw_geometry`] / [`DrawingContext::push_clip`] take any
//!   [`Geometry`]. The code-built geometry types
//!   ([`RectangleGeometry`], [`PathGeometry`](crate::geometry::PathGeometry),
//!   [`EllipseGeometry`](crate::geometry::EllipseGeometry), ...) live in
//!   [`crate::geometry`]; the trait and `RectangleGeometry` are re-exported here
//!   for convenience.
//! * [`DrawingContext`]: a **borrowed** handle over the `DrawingContext*`
//!   handed to a [`crate::classes::RenderHandler`]. Valid only for the duration
//!   of the render callback; the draw / push / pop methods forward straight into
//!   Noesis.
//!
//! Wire a render handler with
//! [`ClassBuilder::set_render`](crate::classes::ClassBuilder::set_render).

use core::marker::PhantomData;
use core::ptr::NonNull;
use std::ffi::{CStr, c_void};

use crate::brushes::Brush;
use crate::ffi::{
    noesis_base_component_release, noesis_drawing_draw_ellipse, noesis_drawing_draw_geometry,
    noesis_drawing_draw_image, noesis_drawing_draw_line, noesis_drawing_draw_mesh,
    noesis_drawing_draw_rectangle, noesis_drawing_draw_rounded_rectangle, noesis_drawing_draw_text,
    noesis_drawing_pop, noesis_drawing_push_blending_mode, noesis_drawing_push_clip,
    noesis_drawing_push_transform, noesis_pen_create, noesis_pen_get_brush,
    noesis_pen_get_dash_offset, noesis_pen_get_dashes, noesis_pen_get_line_caps,
    noesis_pen_get_line_join, noesis_pen_get_thickness, noesis_pen_set_brush,
    noesis_pen_set_dash_style, noesis_pen_set_line_caps, noesis_pen_set_line_join,
    noesis_pen_set_thickness,
};
// Canonical types live in `geometry`; re-exported so the draw / clip calls here
// resolve them without an extra import.
pub use crate::geometry::{Geometry, RectangleGeometry};
pub use crate::shapes::{PenLineCap, PenLineJoin};
use crate::transforms::Transform;

/// How drawn content is mixed with what's behind it. Mirrors
/// `Noesis::BlendingMode`.
#[repr(i32)]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum BlendingMode {
    Normal = 0,
    Multiply = 1,
    Screen = 2,
    Additive = 3,
}

// ── Pen ──────────────────────────────────────────────────────────────────────

/// A code-built `Noesis::Pen`: the stroke (outline) descriptor (a [`Brush`] +
/// thickness + line caps / join) that the `Draw*` calls stroke with.
pub struct Pen {
    ptr: NonNull<c_void>,
}

// SAFETY: Send-only (NOT Sync); see the crate-level "Thread affinity" docs.
unsafe impl Send for Pen {}

impl Pen {
    /// Create a pen of `thickness` painted by `brush` (any [`Brush`]). Noesis
    /// takes its own reference to the brush, so the brush handle may be dropped
    /// afterwards.
    ///
    /// # Panics
    ///
    /// Panics if Noesis fails to allocate the pen (not expected after
    /// [`crate::init`]).
    #[must_use]
    pub fn new(brush: &dyn Brush, thickness: f32) -> Self {
        // SAFETY: brush.brush_raw() is a live Brush* for the borrow; the C side
        // copies the reference. Returns a +1-owned Pen* this handle releases.
        let ptr = unsafe { noesis_pen_create(brush.brush_raw(), thickness) };
        Self {
            ptr: NonNull::new(ptr).expect("noesis_pen_create returned null"),
        }
    }

    /// Create a pen of `thickness` with no brush set yet.
    ///
    /// # Panics
    ///
    /// Panics if Noesis fails to allocate the pen.
    #[must_use]
    pub fn with_thickness(thickness: f32) -> Self {
        // SAFETY: a null brush is allowed (set one later via set_brush).
        let ptr = unsafe { noesis_pen_create(core::ptr::null_mut(), thickness) };
        Self {
            ptr: NonNull::new(ptr).expect("noesis_pen_create returned null"),
        }
    }

    /// Raw `Noesis::Pen*`. Borrowed for the lifetime of `self`.
    #[must_use]
    pub fn raw(&self) -> *mut c_void {
        self.ptr.as_ptr()
    }

    /// Point the pen at `brush` (Noesis takes its own reference).
    #[must_use = "a false return means the property was not set (unknown name / type mismatch / read-only)"]
    pub fn set_brush(&mut self, brush: &dyn Brush) -> bool {
        // SAFETY: self.ptr is a live Pen*; brush_raw() is a live Brush*.
        unsafe { noesis_pen_set_brush(self.ptr.as_ptr(), brush.brush_raw()) }
    }

    /// Borrowed `Noesis::Brush*` currently set on the pen, or `None`. The
    /// pointer has no `+1` reference; do not release it.
    #[must_use]
    pub fn brush(&self) -> Option<NonNull<c_void>> {
        // SAFETY: self.ptr is a live Pen*; the returned pointer is borrowed.
        let p = unsafe { noesis_pen_get_brush(self.ptr.as_ptr()) };
        NonNull::new(p)
    }

    /// Set the stroke thickness (in DIPs).
    #[must_use = "a false return means the property was not set (unknown name / type mismatch / read-only)"]
    pub fn set_thickness(&mut self, thickness: f32) -> bool {
        // SAFETY: self.ptr is a live Pen*.
        unsafe { noesis_pen_set_thickness(self.ptr.as_ptr(), thickness) }
    }

    /// Read the stroke thickness back from the live object.
    #[must_use]
    pub fn thickness(&self) -> f32 {
        let mut out = 0.0f32;
        // SAFETY: self.ptr is a live Pen*; `out` is a valid float.
        unsafe { noesis_pen_get_thickness(self.ptr.as_ptr(), &mut out) };
        out
    }

    /// Set the start, end, and dash line caps.
    #[must_use = "a false return means the property was not set (unknown name / type mismatch / read-only)"]
    pub fn set_line_caps(&mut self, start: PenLineCap, end: PenLineCap, dash: PenLineCap) -> bool {
        // SAFETY: self.ptr is a live Pen*; the enum ordinals match Noesis's.
        unsafe {
            noesis_pen_set_line_caps(self.ptr.as_ptr(), start as i32, end as i32, dash as i32)
        }
    }

    /// Read `(start, end, dash)` line caps back from the live object.
    #[must_use]
    pub fn line_caps(&self) -> Option<(PenLineCap, PenLineCap, PenLineCap)> {
        let mut out = [0i32; 3];
        // SAFETY: self.ptr is a live Pen*; `out` is a 3-int buffer.
        let ok = unsafe { noesis_pen_get_line_caps(self.ptr.as_ptr(), out.as_mut_ptr()) };
        if !ok {
            return None;
        }
        Some((
            cap_from_i32(out[0]),
            cap_from_i32(out[1]),
            cap_from_i32(out[2]),
        ))
    }

    /// Set the line join and miter limit.
    #[must_use = "a false return means the property was not set (unknown name / type mismatch / read-only)"]
    pub fn set_line_join(&mut self, join: PenLineJoin, miter_limit: f32) -> bool {
        // SAFETY: self.ptr is a live Pen*; the enum ordinal matches Noesis's.
        unsafe { noesis_pen_set_line_join(self.ptr.as_ptr(), join as i32, miter_limit) }
    }

    /// Read `(join, miter_limit)` back from the live object.
    #[must_use]
    pub fn line_join(&self) -> Option<(PenLineJoin, f32)> {
        let mut join = 0i32;
        let mut miter = 0.0f32;
        // SAFETY: self.ptr is a live Pen*; both out params are valid.
        let ok = unsafe { noesis_pen_get_line_join(self.ptr.as_ptr(), &mut join, &mut miter) };
        ok.then(|| (join_from_i32(join), miter))
    }

    /// Set a typed dash pattern on this pen, building a `Noesis::DashStyle` from
    /// `dashes` (alternating dash / gap lengths, in multiples of the pen
    /// thickness) and `offset` (how far into the pattern the stroke begins).
    /// Passing an empty `dashes` slice clears the dash style, restoring a solid
    /// stroke.
    ///
    /// This is the typed `&[f32]` companion to the shape stroke-dash string path
    /// ([`Shape::set_stroke_dash_array`](crate::shapes::Shape::set_stroke_dash_array)):
    /// here the lengths cross the FFI as a `f32` array and Noesis's
    /// space-separated `DashStyle.Dashes` string is built natively.
    pub fn set_dash_style(&mut self, dashes: &[f32], offset: f32) -> bool {
        let count = u32::try_from(dashes.len()).unwrap_or(u32::MAX);
        // SAFETY: self.ptr is a live Pen*; `dashes`/`count` describe a valid
        // (possibly empty) slice read only for the duration of the call.
        unsafe { noesis_pen_set_dash_style(self.ptr.as_ptr(), dashes.as_ptr(), count, offset) }
    }

    /// Read the dash `offset` back from the live object, or `None` if no dash
    /// style is set (a solid stroke).
    #[must_use]
    pub fn dash_offset(&self) -> Option<f32> {
        let mut out = 0.0f32;
        // SAFETY: self.ptr is a live Pen*; `out` is a valid float.
        let ok = unsafe { noesis_pen_get_dash_offset(self.ptr.as_ptr(), &mut out) };
        ok.then_some(out)
    }

    /// Read the dash pattern back from the live object as a typed `Vec<f32>`,
    /// re-parsed from Noesis's `DashStyle.Dashes` string. `None` if no dash
    /// style is set.
    #[must_use]
    pub fn dashes(&self) -> Option<Vec<f32>> {
        // SAFETY: self.ptr is a live Pen*; the returned pointer (if non-null) is
        // a borrowed NUL-terminated string valid until the next pen mutation,
        // copied out immediately here.
        let p = unsafe { noesis_pen_get_dashes(self.ptr.as_ptr()) };
        if p.is_null() {
            return None;
        }
        // SAFETY: p is a NUL-terminated string owned by the live DashStyle.
        let s = unsafe { CStr::from_ptr(p) }.to_string_lossy();
        Some(
            s.split([' ', ','])
                .filter(|t| !t.is_empty())
                .filter_map(|t| t.parse::<f32>().ok())
                .collect(),
        )
    }
}

impl Drop for Pen {
    fn drop(&mut self) {
        // SAFETY: produced by noesis_pen_create with a +1 ref we own.
        unsafe { noesis_base_component_release(self.ptr.as_ptr()) }
    }
}

fn cap_from_i32(v: i32) -> PenLineCap {
    match v {
        1 => PenLineCap::Square,
        2 => PenLineCap::Round,
        3 => PenLineCap::Triangle,
        _ => PenLineCap::Flat,
    }
}

fn join_from_i32(v: i32) -> PenLineJoin {
    match v {
        1 => PenLineJoin::Bevel,
        2 => PenLineJoin::Round,
        _ => PenLineJoin::Miter,
    }
}

// ── DrawingContext ───────────────────────────────────────────────────────────

/// A **borrowed** drawing context, valid only for the duration of a
/// [`crate::classes::RenderHandler::render`] callback. Issues immediate-mode
/// draw / push / pop commands straight into Noesis. Do not store it past the
/// callback; the underlying `Noesis::DrawingContext*` is owned by the element's
/// render pass.
///
/// Coordinates are in DIPs in the element's local space. A null `brush` (`None`)
/// fills nothing; a null `pen` (`None`) strokes nothing, matching Noesis's own
/// behaviour, so passing both `None` draws nothing.
pub struct DrawingContext<'a> {
    ptr: NonNull<c_void>,
    _marker: PhantomData<&'a ()>,
}

impl DrawingContext<'_> {
    /// Wrap a borrowed `Noesis::DrawingContext*` received via the FFI render
    /// callback.
    ///
    /// # Safety
    ///
    /// `ptr` must be the non-null context pointer delivered to the render
    /// callback; it is borrowed and valid only for that call.
    #[must_use]
    pub unsafe fn from_raw(ptr: NonNull<c_void>) -> Self {
        Self {
            ptr,
            _marker: PhantomData,
        }
    }

    /// Raw `Noesis::DrawingContext*`. Borrowed for the lifetime of `self`.
    #[must_use]
    pub fn raw(&self) -> *mut c_void {
        self.ptr.as_ptr()
    }

    /// Draw a line between two points with `pen`.
    pub fn draw_line(&self, pen: &Pen, p0: (f32, f32), p1: (f32, f32)) -> bool {
        // SAFETY: self.ptr is a live DrawingContext*; pen.raw() is a live Pen*.
        unsafe { noesis_drawing_draw_line(self.ptr.as_ptr(), pen.raw(), p0.0, p0.1, p1.0, p1.1) }
    }

    /// Fill and/or stroke a rectangle `[x, y, w, h]`.
    pub fn draw_rectangle(
        &self,
        brush: Option<&dyn Brush>,
        pen: Option<&Pen>,
        rect: [f32; 4],
    ) -> bool {
        // SAFETY: self.ptr is a live DrawingContext*; the brush / pen pointers
        // (or null) are live for the borrow.
        unsafe {
            noesis_drawing_draw_rectangle(
                self.ptr.as_ptr(),
                brush_ptr(brush),
                pen_ptr(pen),
                rect[0],
                rect[1],
                rect[2],
                rect[3],
            )
        }
    }

    /// Fill and/or stroke a rounded rectangle `[x, y, w, h]` with corner radii
    /// `(r_x, r_y)`.
    pub fn draw_rounded_rectangle(
        &self,
        brush: Option<&dyn Brush>,
        pen: Option<&Pen>,
        rect: [f32; 4],
        r_x: f32,
        r_y: f32,
    ) -> bool {
        // SAFETY: as `draw_rectangle`.
        unsafe {
            noesis_drawing_draw_rounded_rectangle(
                self.ptr.as_ptr(),
                brush_ptr(brush),
                pen_ptr(pen),
                rect[0],
                rect[1],
                rect[2],
                rect[3],
                r_x,
                r_y,
            )
        }
    }

    /// Fill and/or stroke an ellipse centered at `(cx, cy)` with radii
    /// `(r_x, r_y)`.
    pub fn draw_ellipse(
        &self,
        brush: Option<&dyn Brush>,
        pen: Option<&Pen>,
        center: (f32, f32),
        r_x: f32,
        r_y: f32,
    ) -> bool {
        // SAFETY: as `draw_rectangle`.
        unsafe {
            noesis_drawing_draw_ellipse(
                self.ptr.as_ptr(),
                brush_ptr(brush),
                pen_ptr(pen),
                center.0,
                center.1,
                r_x,
                r_y,
            )
        }
    }

    /// Fill and/or stroke a [`Geometry`].
    pub fn draw_geometry(
        &self,
        brush: Option<&dyn Brush>,
        pen: Option<&Pen>,
        geometry: &dyn Geometry,
    ) -> bool {
        // SAFETY: as `draw_rectangle`; geometry_raw() is a live Geometry*.
        unsafe {
            noesis_drawing_draw_geometry(
                self.ptr.as_ptr(),
                brush_ptr(brush),
                pen_ptr(pen),
                geometry.geometry_raw(),
            )
        }
    }

    /// Draw a [`FormattedText`](crate::formatted_text::FormattedText) into the
    /// bounds rect `[x, y, w, h]`. The text's foreground brush is baked into the
    /// `FormattedText` at construction, so there is no brush argument here.
    /// Returns `false` only if the context cast fails.
    pub fn draw_text(
        &self,
        formatted_text: &crate::formatted_text::FormattedText,
        bounds: [f32; 4],
    ) -> bool {
        // SAFETY: self.ptr is a live DrawingContext*; raw() is a live
        // FormattedText* borrowed for the call.
        unsafe {
            noesis_drawing_draw_text(
                self.ptr.as_ptr(),
                formatted_text.raw(),
                bounds[0],
                bounds[1],
                bounds[2],
                bounds[3],
            )
        }
    }

    /// Fill a [`MeshData`](crate::mesh::MeshData) with an optional `brush`
    /// (`None` paints nothing). Returns `false` if the context cast fails.
    pub fn draw_mesh(&self, brush: Option<&dyn Brush>, mesh: &crate::mesh::MeshData) -> bool {
        // SAFETY: self.ptr is a live DrawingContext*; mesh.raw() is a live
        // MeshData*; the brush pointer (or null) is live for the borrow.
        unsafe { noesis_drawing_draw_mesh(self.ptr.as_ptr(), brush_ptr(brush), mesh.raw()) }
    }

    /// Draw a borrowed `Noesis::ImageSource*` into `[x, y, w, h]`. Returns
    /// `false` if `image_source` is null / not an `ImageSource`.
    ///
    /// # Safety
    ///
    /// `image_source` must be a live `Noesis::ImageSource*` (e.g. from
    /// [`FrameworkElement::get_component`](crate::view::FrameworkElement::get_component)).
    pub unsafe fn draw_image(&self, image_source: *mut c_void, rect: [f32; 4]) -> bool {
        // SAFETY: self.ptr is a live DrawingContext*; `image_source` per contract.
        unsafe {
            noesis_drawing_draw_image(
                self.ptr.as_ptr(),
                image_source,
                rect[0],
                rect[1],
                rect[2],
                rect[3],
            )
        }
    }

    /// Pop the last `push_*` operation off the context.
    pub fn pop(&self) -> bool {
        // SAFETY: self.ptr is a live DrawingContext*.
        unsafe { noesis_drawing_pop(self.ptr.as_ptr()) }
    }

    /// Push a clip [`Geometry`]; pair with [`Self::pop`].
    pub fn push_clip(&self, geometry: &dyn Geometry) -> bool {
        // SAFETY: self.ptr is a live DrawingContext*; geometry_raw() is live.
        unsafe { noesis_drawing_push_clip(self.ptr.as_ptr(), geometry.geometry_raw()) }
    }

    /// Push a [`Transform`]; pair with [`Self::pop`].
    pub fn push_transform(&self, transform: &dyn Transform) -> bool {
        // SAFETY: self.ptr is a live DrawingContext*; transform_raw() is live.
        unsafe { noesis_drawing_push_transform(self.ptr.as_ptr(), transform.transform_raw()) }
    }

    /// Push a [`BlendingMode`]; pair with [`Self::pop`].
    pub fn push_blending_mode(&self, mode: BlendingMode) -> bool {
        // SAFETY: self.ptr is a live DrawingContext*; the ordinal matches Noesis.
        unsafe { noesis_drawing_push_blending_mode(self.ptr.as_ptr(), mode as i32) }
    }
}

fn brush_ptr(brush: Option<&dyn Brush>) -> *mut c_void {
    brush.map_or(core::ptr::null_mut(), Brush::brush_raw)
}

fn pen_ptr(pen: Option<&Pen>) -> *mut c_void {
    pen.map_or(core::ptr::null_mut(), Pen::raw)
}