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
// ICommand-from-Rust bridge.
//
// A `RustCommand : Noesis::BaseCommand` trampoline lets XAML
// `Command="{Binding ...}"` invoke Rust logic. `BaseCommand` already
// implements the `ICommand` interface (CanExecute / Execute / the
// CanExecuteChanged EventHandler) and exposes `RaiseCanExecuteChanged()`;
// we only override CanExecute / Execute to forward into a Rust vtable and
// re-expose the raise so bound controls re-query (drives button
// enable/disable).
//
// Lifetime: unlike the synthetic-class / markup trampolines (which need a
// refcounted side `ClassData` because one registration backs many
// instances), a command is 1:1 with its Rust handler box. The box is owned
// directly by the `RustCommand` instance and freed in its destructor. The
// instance is an ordinary `BaseComponent`, so Noesis's intrusive refcount
// guarantees the destructor (and therefore the free handler) runs exactly
// once, after the LAST reference drops. That last reference may be the
// binding (Button.Command) holding the command alive well past the Rust
// `Command` handle being dropped, so CanExecute / Execute keep working until
// the visual tree lets go.

#include "noesis_shim.h"

#include <NsCore/Delegate.h>
#include <NsCore/DynamicCast.h>
#include <NsCore/Noesis.h>
#include <NsCore/Ptr.h>
#include <NsCore/Reflection.h>
#include <NsCore/ReflectionImplement.h>
#include <NsCore/Symbol.h>
#include <NsCore/Type.h>
#include <NsCore/TypeClass.h>
#include <NsGui/ApplicationCommands.h>
#include <NsGui/BaseCommand.h>
#include <NsGui/CommandBinding.h>
#include <NsGui/ComponentCommands.h>
#include <NsGui/RoutedCommand.h>
#include <NsGui/RoutedUICommand.h>
#include <NsGui/UICollection.h>
#include <NsGui/UIElement.h>

namespace {

class RustCommand final: public Noesis::BaseCommand {
public:
    RustCommand(const noesis_command_vtable* vt, void* userdata,
                noesis_command_free_fn free_handler)
        : mVtable(*vt), mUserdata(userdata), mFree(free_handler) {}

    ~RustCommand() {
        // Donated ownership: the Rust handler box is dropped here, exactly
        // once, when the final BaseComponent reference goes away. Null the
        // pointer first so a (currently-impossible) re-entrant teardown
        // can't double-free.
        void* ud = mUserdata;
        mUserdata = nullptr;
        if (mFree && ud) {
            mFree(ud);
        }
    }

    // From ICommand (via BaseCommand). `param` is the borrowed command
    // parameter BaseComponent* (may be null). Forwarded verbatim.
    bool CanExecute(Noesis::BaseComponent* param) const override {
        if (mVtable.can_execute) {
            return mVtable.can_execute(mUserdata, param);
        }
        return true;
    }

    void Execute(Noesis::BaseComponent* param) const override {
        if (mVtable.execute) {
            mVtable.execute(mUserdata, param);
        }
    }

    NS_IMPLEMENT_INLINE_REFLECTION(RustCommand, Noesis::BaseCommand, "DmNoesis.RustCommand") {}

private:
    noesis_command_vtable  mVtable;
    void*                     mUserdata;
    noesis_command_free_fn mFree;
};

}  // namespace

// ── C ABI surface ──────────────────────────────────────────────────────────

extern "C" void* noesis_command_create(
    const noesis_command_vtable* vt,
    void* userdata,
    noesis_command_free_fn free_handler) {
    if (!vt) return nullptr;
    // BaseRefCounted starts at refcount 1. That initial reference IS the
    // caller's +1, balanced by noesis_command_destroy. (No AddReference:
    // a binding that later stores the command takes its own ref via
    // SetValueObject, so the handler box outlives our destroy until that ref
    // also drops.)
    auto* cmd = new RustCommand(vt, userdata, free_handler);
    return static_cast<Noesis::BaseComponent*>(cmd);
}

extern "C" void noesis_command_destroy(void* command) {
    if (!command) return;
    static_cast<Noesis::BaseComponent*>(command)->Release();
}

extern "C" void noesis_command_raise_can_execute_changed(void* command) {
    if (!command) return;
    auto* cmd = Noesis::DynamicCast<Noesis::BaseCommand*>(
        static_cast<Noesis::BaseComponent*>(command));
    if (cmd) {
        cmd->RaiseCanExecuteChanged();
    }
}

// ── RoutedCommand / RoutedUICommand ─────────────────────────────────────────
//
// A RoutedCommand routes Execute / CanExecute through the element tree to the
// first matching CommandBinding (below). Construction needs an owner TypeClass;
// we resolve it from a type name through the Core reflection registry (a
// built-in like "UIElement" or a registered custom class). Both are
// BaseCommand-derived, so noesis_command_raise_can_execute_changed works on
// them too. Returned commands carry +1 (release via
// noesis_base_component_release).

namespace {
const Noesis::TypeClass* resolve_owner(const char* owner_type_name) {
    if (!owner_type_name) return nullptr;
    const Noesis::Type* t = Noesis::Reflection::GetType(Noesis::Symbol(owner_type_name));
    return Noesis::DynamicCast<const Noesis::TypeClass*>(t);
}
}  // namespace

extern "C" void* noesis_routed_command_create(const char* name, const char* owner_type_name) {
    if (!name) return nullptr;
    const Noesis::TypeClass* owner = resolve_owner(owner_type_name);
    if (!owner) return nullptr;
    // BaseRefCounted starts at refcount 1. That initial ref is the caller's +1.
    auto* cmd = new Noesis::RoutedCommand(Noesis::Symbol(name), owner);
    return static_cast<Noesis::BaseComponent*>(cmd);
}

extern "C" void* noesis_routed_ui_command_create(
    const char* name, const char* text, const char* owner_type_name) {
    if (!name) return nullptr;
    const Noesis::TypeClass* owner = resolve_owner(owner_type_name);
    if (!owner) return nullptr;
    auto* cmd = new Noesis::RoutedUICommand(text ? text : "", Noesis::Symbol(name), owner);
    return static_cast<Noesis::BaseComponent*>(cmd);
}

extern "C" void noesis_routed_command_execute(void* command, void* param, void* target) {
    auto* cmd = Noesis::DynamicCast<Noesis::RoutedCommand*>(
        static_cast<Noesis::BaseComponent*>(command));
    auto* ui = Noesis::DynamicCast<Noesis::UIElement*>(
        static_cast<Noesis::BaseComponent*>(target));
    if (cmd && ui) {
        cmd->Execute(static_cast<Noesis::BaseComponent*>(param), ui);
    }
}

extern "C" bool noesis_routed_command_can_execute(void* command, void* param, void* target) {
    auto* cmd = Noesis::DynamicCast<Noesis::RoutedCommand*>(
        static_cast<Noesis::BaseComponent*>(command));
    auto* ui = Noesis::DynamicCast<Noesis::UIElement*>(
        static_cast<Noesis::BaseComponent*>(target));
    if (cmd && ui) {
        return cmd->CanExecute(static_cast<Noesis::BaseComponent*>(param), ui);
    }
    return false;
}

// Registered name (RoutedCommand::GetName), borrowed (interned Symbol string).
extern "C" const char* noesis_routed_command_get_name(void* command) {
    auto* cmd = Noesis::DynamicCast<Noesis::RoutedCommand*>(
        static_cast<Noesis::BaseComponent*>(command));
    return cmd ? cmd->GetName().Str() : nullptr;
}

extern "C" const char* noesis_routed_ui_command_get_text(void* command) {
    auto* cmd = Noesis::DynamicCast<Noesis::RoutedUICommand*>(
        static_cast<Noesis::BaseComponent*>(command));
    return cmd ? cmd->GetText() : nullptr;
}

extern "C" void noesis_routed_ui_command_set_text(void* command, const char* text) {
    auto* cmd = Noesis::DynamicCast<Noesis::RoutedUICommand*>(
        static_cast<Noesis::BaseComponent*>(command));
    if (cmd) {
        cmd->SetText(text ? text : "");
    }
}

// ── CommandBinding ───────────────────────────────────────────────────────────
//
// Binds a command to Rust Executed / CanExecute handlers and attaches to an
// element's CommandBindings, so an invoked RoutedCommand (or built-in) routing
// through that element fires the handler. Lifetime mirrors the routed-event
// bridges (noesis_events.cpp): a heap RustCommandBinding owns the donated Rust
// box + a +1 on the CommandBinding, registers the delegates with `+=`, and
// detaches with `-=` in its destructor.

namespace {

class RustCommandBinding {
public:
    RustCommandBinding(Noesis::ICommand* command,
                       noesis_cmd_executed_fn executed,
                       noesis_cmd_can_execute_fn can_execute,
                       void* userdata, noesis_command_free_fn free_handler)
        : mExecuted(executed), mCanExecute(can_execute), mUserdata(userdata),
          mFree(free_handler) {
        mBinding = *new Noesis::CommandBinding(command);
        mBinding->Executed() += Noesis::MakeDelegate(this, &RustCommandBinding::OnExecuted);
        mBinding->CanExecute() += Noesis::MakeDelegate(this, &RustCommandBinding::OnCanExecute);
    }

    ~RustCommandBinding() {
        mBinding->Executed() -= Noesis::MakeDelegate(this, &RustCommandBinding::OnExecuted);
        mBinding->CanExecute() -= Noesis::MakeDelegate(this, &RustCommandBinding::OnCanExecute);
        // Reverse attach: remove the (now inert) binding from the element's
        // CommandBindings so it doesn't accumulate there for the element's life.
        if (mAttached) {
            if (auto* bindings = mAttached->GetCommandBindings()) {
                bindings->Remove(mBinding);
            }
            mAttached.Reset();
        }
        void* ud = mUserdata;
        mUserdata = nullptr;
        if (mFree && ud) {
            mFree(ud);
        }
        // Drop our +1 on the CommandBinding (the element's collection, if still
        // attached elsewhere, holds its own ref and keeps it alive as needed).
        mBinding.Reset();
    }

    RustCommandBinding(const RustCommandBinding&) = delete;
    RustCommandBinding& operator=(const RustCommandBinding&) = delete;

    Noesis::CommandBinding* binding() const { return mBinding; }

    // Record the element attach added the binding to, holding a +1 ref so the
    // destructor can remove the binding from its collection again.
    void setAttached(Noesis::UIElement* element) {
        mAttached.Reset(element);
    }

    // True => an Executed / CanExecute callback is on the stack, so destruction
    // was deferred (the outermost dispatch frame deletes); the caller must NOT
    // delete. A command binding may drop itself (Rust Drop -> destroy) from
    // inside its own callback, and deleting `this` mid-dispatch would be a
    // use-after-free. A depth counter (not a bool) tracks nesting because a
    // callback may synchronously re-invoke the command and re-enter. Thread-
    // affine to the view-driving thread, so no atomics are needed.
    bool deferDeleteIfDispatching() {
        if (mDispatchDepth > 0) {
            mPendingDelete = true;
            return true;
        }
        return false;
    }

private:
    void OnExecuted(Noesis::BaseComponent*, const Noesis::ExecutedRoutedEventArgs& args) {
        mDispatchDepth++;
        if (mExecuted) {
            mExecuted(mUserdata, args.parameter);
        }
        args.handled = true;
        if (--mDispatchDepth == 0 && mPendingDelete) {
            delete this;  // deferred teardown from a destroy during dispatch
        }
    }

    void OnCanExecute(Noesis::BaseComponent*, const Noesis::CanExecuteRoutedEventArgs& args) {
        mDispatchDepth++;
        bool can = true;
        if (mCanExecute) {
            can = mCanExecute(mUserdata, args.parameter);
        }
        args.canExecute = can;
        args.handled = true;
        if (--mDispatchDepth == 0 && mPendingDelete) {
            delete this;  // deferred teardown from a destroy during dispatch
        }
    }

    Noesis::Ptr<Noesis::CommandBinding> mBinding;
    Noesis::Ptr<Noesis::UIElement> mAttached;  // element attach registered with, or null.
    noesis_cmd_executed_fn    mExecuted;
    noesis_cmd_can_execute_fn mCanExecute;
    void*                        mUserdata;
    noesis_command_free_fn    mFree;
    uint32_t mDispatchDepth = 0;
    bool mPendingDelete = false;
};

}  // namespace

extern "C" void* noesis_command_binding_create(
    void* command, noesis_cmd_executed_fn executed,
    noesis_cmd_can_execute_fn can_execute, void* userdata,
    noesis_command_free_fn free_handler) {
    if (!command) return nullptr;
    auto* cmd = Noesis::DynamicCast<Noesis::ICommand*>(
        static_cast<Noesis::BaseComponent*>(command));
    if (!cmd) return nullptr;
    return new RustCommandBinding(cmd, executed, can_execute, userdata, free_handler);
}

extern "C" bool noesis_command_binding_attach(void* token, void* element) {
    if (!token || !element) return false;
    auto* ui = Noesis::DynamicCast<Noesis::UIElement*>(
        static_cast<Noesis::BaseComponent*>(element));
    if (!ui) return false;
    auto* bridge = static_cast<RustCommandBinding*>(token);
    ui->GetCommandBindings()->Add(bridge->binding());
    bridge->setAttached(ui);
    return true;
}

extern "C" void noesis_command_binding_destroy(void* token) {
    if (!token) return;
    // Detaches the delegates, removes the binding from the attached element,
    // frees the donated box, drops our binding ref. Deferred if a callback for
    // this binding is currently on the dispatch stack (the epilogue deletes).
    auto* bridge = static_cast<RustCommandBinding*>(token);
    if (bridge->deferDeleteIfDispatching()) return;
    delete bridge;
}

// ── Built-in command libraries ───────────────────────────────────────────────
//
// Borrowed `const RoutedUICommand*` singletons owned by the framework. Do NOT
// release. Indexed by the enums in src/commands.rs; the switch evaluates each
// static at call time (after GUI init), so the pointers are live. NULL on an
// out-of-range index.

extern "C" const void* noesis_application_command(uint32_t which) {
    using AC = Noesis::ApplicationCommands;
    switch (which) {
        case 0:  return AC::CancelPrintCommand;
        case 1:  return AC::CloseCommand;
        case 2:  return AC::ContextMenuCommand;
        case 3:  return AC::CopyCommand;
        case 4:  return AC::CorrectionListCommand;
        case 5:  return AC::CutCommand;
        case 6:  return AC::DeleteCommand;
        case 7:  return AC::FindCommand;
        case 8:  return AC::HelpCommand;
        case 9:  return AC::NewCommand;
        case 10: return AC::OpenCommand;
        case 11: return AC::PasteCommand;
        case 12: return AC::PrintCommand;
        case 13: return AC::PrintPreviewCommand;
        case 14: return AC::PropertiesCommand;
        case 15: return AC::RedoCommand;
        case 16: return AC::ReplaceCommand;
        case 17: return AC::SaveCommand;
        case 18: return AC::SaveAsCommand;
        case 19: return AC::SelectAllCommand;
        case 20: return AC::StopCommand;
        case 21: return AC::UndoCommand;
        default: return nullptr;
    }
}

extern "C" const void* noesis_component_command(uint32_t which) {
    using CC = Noesis::ComponentCommands;
    switch (which) {
        case 0:  return CC::ExtendSelectionDownCommand;
        case 1:  return CC::ExtendSelectionLeftCommand;
        case 2:  return CC::ExtendSelectionRightCommand;
        case 3:  return CC::ExtendSelectionUpCommand;
        case 4:  return CC::MoveDownCommand;
        case 5:  return CC::MoveFocusBackCommand;
        case 6:  return CC::MoveFocusDownCommand;
        case 7:  return CC::MoveFocusForwardCommand;
        case 8:  return CC::MoveFocusPageDownCommand;
        case 9:  return CC::MoveFocusPageUpCommand;
        case 10: return CC::MoveFocusUpCommand;
        case 11: return CC::MoveLeftCommand;
        case 12: return CC::MoveRightCommand;
        case 13: return CC::MoveToEndCommand;
        case 14: return CC::MoveToHomeCommand;
        case 15: return CC::MoveToPageDownCommand;
        case 16: return CC::MoveToPageUpCommand;
        case 17: return CC::MoveUpCommand;
        case 18: return CC::ScrollByLineCommand;
        case 19: return CC::ScrollPageDownCommand;
        case 20: return CC::ScrollPageLeftCommand;
        case 21: return CC::ScrollPageRightCommand;
        case 22: return CC::ScrollPageUpCommand;
        case 23: return CC::SelectToEndCommand;
        case 24: return CC::SelectToHomeCommand;
        case 25: return CC::SelectToPageDownCommand;
        case 26: return CC::SelectToPageUpCommand;
        default: return nullptr;
    }
}