wxdragon 0.9.15

Safe Rust bindings for wxWidgets via the wxDragon C wrapper
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
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
//! wxMenu wrapper

use crate::id::Id;
use crate::menus::menuitem::{ItemKind, MenuItem};
#[cfg(feature = "xrc")]
use crate::window::WindowHandle;
use crate::{CommandEventData, Event, EventType};
use std::ffi::{CStr, CString};
use std::marker::PhantomData;
use std::rc::Rc;
use wxdragon_sys as ffi;

/// Safe Rust wrapper over a wxMenu pointer (wxd_Menu_t).
///
/// By default, instances created via builder or From<*mut> own the underlying native resource
/// and will destroy it on drop. Instances created from a const pointer are treated as borrowed
/// and will not destroy the underlying object.
pub struct Menu {
    ptr: *mut ffi::wxd_Menu_t,
    /// Whether this wrapper owns the underlying wxMenu and should destroy it on drop.
    owned: bool,
    /// Prevent Send/Sync: wxWidgets objects are not thread-safe and must stay on UI thread.
    _nosend_nosync: PhantomData<Rc<()>>,
}

impl Drop for Menu {
    fn drop(&mut self) {
        self.destroy_menu();
    }
}

impl From<*mut ffi::wxd_Menu_t> for Menu {
    fn from(ptr: *mut ffi::wxd_Menu_t) -> Self {
        assert!(!ptr.is_null(), "invalid null pointer passed to Menu::from");
        Menu {
            ptr,
            owned: true,
            _nosend_nosync: PhantomData,
        }
    }
}

impl From<*const ffi::wxd_Menu_t> for Menu {
    fn from(ptr: *const ffi::wxd_Menu_t) -> Self {
        assert!(!ptr.is_null(), "invalid null pointer passed to Menu::from");
        let ptr = ptr as *mut ffi::wxd_Menu_t;
        Menu {
            ptr,
            owned: false,
            _nosend_nosync: PhantomData,
        }
    }
}

// Pointer conversions mirroring Variant
impl TryFrom<Menu> for *const ffi::wxd_Menu_t {
    type Error = std::io::Error;
    fn try_from(value: Menu) -> Result<Self, Self::Error> {
        use std::io::{Error, ErrorKind::InvalidData, ErrorKind::InvalidInput};
        if value.ptr.is_null() {
            return Err(Error::new(InvalidInput, "Menu pointer is null"));
        }
        if value.is_owned() {
            return Err(Error::new(
                InvalidData,
                "Menu owns the pointer, use into_raw_mut or mutable version",
            ));
        }
        let ptr = value.ptr as *const _;
        std::mem::forget(value);
        Ok(ptr)
    }
}

impl TryFrom<Menu> for *mut ffi::wxd_Menu_t {
    type Error = std::io::Error;
    fn try_from(value: Menu) -> Result<Self, Self::Error> {
        use std::io::{Error, ErrorKind::InvalidData, ErrorKind::InvalidInput};
        if value.ptr.is_null() {
            return Err(Error::new(InvalidInput, "Menu pointer is null"));
        }
        if !value.is_owned() {
            return Err(Error::new(InvalidData, "Menu does not own the pointer, use const version"));
        }
        let ptr = value.ptr;
        std::mem::forget(value);
        Ok(ptr)
    }
}

impl Menu {
    /// Creates a new, empty menu using the builder pattern.
    pub fn builder() -> MenuBuilder {
        MenuBuilder::new()
    }

    /// Returns whether this wrapper owns the underlying wxMenu.
    pub fn is_owned(&self) -> bool {
        self.owned
    }

    /// Gets the number of items in the menu.
    pub fn get_item_count(&self) -> usize {
        unsafe { ffi::wxd_Menu_GetMenuItemCount(self.ptr) }
    }

    /// Gets the title of the menu.
    pub fn get_title(&self) -> Option<String> {
        // First, get the required buffer size
        let size = unsafe { ffi::wxd_Menu_GetTitle(self.ptr, std::ptr::null_mut(), 0) };
        if size < 0 {
            return None;
        }

        let mut buffer = vec![0; size as usize + 1]; // +1 for null terminator
        unsafe { ffi::wxd_Menu_GetTitle(self.ptr, buffer.as_mut_ptr(), buffer.len()) };
        Some(unsafe { CStr::from_ptr(buffer.as_ptr()).to_string_lossy().to_string() })
    }

    pub fn set_title(&mut self, title: &str) {
        let title_c = CString::new(title).unwrap_or_default();
        unsafe { ffi::wxd_Menu_SetTitle(self.as_mut_ptr(), title_c.as_ptr()) };
    }

    /// Explicitly destroy this Menu. Use this for standalone/popup menus that are not
    /// appended to a MenuBar. After calling this method, the Menu must not be used.
    ///
    /// Safety: Do NOT call this if the menu was appended to a MenuBar, as the menubar
    /// takes ownership and will delete it, leading to double free.
    pub fn destroy_menu(&mut self) {
        if self.owned && !self.ptr.is_null() {
            log::debug!("Menu '{:?}' destroyed", self.get_title());
            unsafe { ffi::wxd_Menu_Destroy(self.ptr) };
            self.ptr = std::ptr::null_mut();
        }
    }

    /// Returns a const raw pointer to the underlying wxMenu.
    /// This does not transfer ownership and is only valid while self is alive.
    pub fn as_const_ptr(&self) -> *const ffi::wxd_Menu_t {
        self.ptr as *const _
    }

    /// Returns a mutable raw pointer to the underlying wxMenu.
    /// This does not transfer ownership.
    pub fn as_mut_ptr(&mut self) -> *mut ffi::wxd_Menu_t {
        self.ptr
    }

    /// Consumes self and returns a raw mutable pointer, transferring ownership to the caller.
    /// After calling this, you must destroy the pointer exactly once using wxd_Menu_Destroy.
    pub fn into_raw_mut(self) -> *mut ffi::wxd_Menu_t {
        self.try_into().expect("into_raw_mut must only be used on owning wrappers")
    }

    /// Consumes a borrowed (non-owning) wrapper and returns a raw const pointer without taking ownership.
    /// Panics if called on an owning wrapper to avoid leaking the owned resource.
    pub fn into_raw_const(self) -> *const ffi::wxd_Menu_t {
        self.try_into()
            .expect("into_raw_const must only be used on non-owning wrappers")
    }

    /// Appends a menu item.
    /// Returns a wrapper for the created item (for potential modification), but ownership remains with the menu.
    pub fn append(&self, id: Id, item: &str, help_string: &str, kind: ItemKind) -> Option<MenuItem> {
        self.append_raw(id, item, help_string, kind)
    }

    /// Appends a submenu.
    pub fn append_submenu(&self, submenu: Menu, title: &str, help_string: &str) -> Option<MenuItem> {
        let title = CString::new(title).unwrap_or_default();
        let help_c = CString::new(help_string).unwrap_or_default();
        let item_ptr = unsafe { ffi::wxd_Menu_AppendSubMenu(self.ptr, submenu.into_raw_mut(), title.as_ptr(), help_c.as_ptr()) };
        if item_ptr.is_null() {
            return None;
        }
        // Return a MenuItem wrapper, but don't give it ownership
        Some(MenuItem::from(item_ptr))
    }

    /// Enables or disables a menu item by its ID.
    /// Returns true if the operation was successful.
    pub fn enable_item(&self, id: Id, enable: bool) -> bool {
        unsafe { ffi::wxd_Menu_ItemEnable(self.ptr, id, enable) }
    }

    /// Checks if a menu item is enabled.
    pub fn is_item_enabled(&self, id: Id) -> bool {
        unsafe { ffi::wxd_Menu_IsItemEnabled(self.ptr, id) }
    }

    /// Checks or unchecks a menu item by its ID.
    pub fn check_item(&self, id: Id, check: bool) {
        unsafe { ffi::wxd_Menu_CheckItem(self.ptr, id, check) }
    }

    /// Checks if a menu item is checked.
    pub fn is_item_checked(&self, id: Id) -> bool {
        unsafe { ffi::wxd_Menu_IsItemChecked(self.ptr, id) }
    }

    /// Finds a menu item by its ID.
    /// Returns the found MenuItem or None if not found.
    pub fn find_item(&self, id: Id) -> Option<MenuItem> {
        let ptr = unsafe { ffi::wxd_Menu_FindItem(self.ptr, id) };
        if ptr.is_null() { None } else { Some(MenuItem::from_ptr(ptr)) }
    }

    /// Appends a separator.
    pub fn append_separator(&self) {
        self.append_separator_raw();
    }

    /// Inserts a menu item at a specific position.
    pub fn insert(&self, pos: usize, id: Id, item: &str, help_string: &str, kind: ItemKind) -> Option<MenuItem> {
        let item_c = CString::new(item).unwrap_or_default();
        let help_c = CString::new(help_string).unwrap_or_default();
        let item_ptr = unsafe { ffi::wxd_Menu_Insert(self.ptr, pos, id, item_c.as_ptr(), help_c.as_ptr(), kind.into()) };
        if item_ptr.is_null() {
            None
        } else {
            Some(MenuItem::from_ptr(item_ptr))
        }
    }

    /// Inserts a submenu at a specific position.
    pub fn insert_submenu(&self, pos: usize, submenu: Menu, title: &str, help_string: &str) -> Option<MenuItem> {
        let title = CString::new(title).unwrap_or_default();
        let help_c = CString::new(help_string).unwrap_or_default();
        let item_ptr =
            unsafe { ffi::wxd_Menu_InsertSubMenu(self.ptr, pos, submenu.into_raw_mut(), title.as_ptr(), help_c.as_ptr()) };
        if item_ptr.is_null() {
            return None;
        }
        Some(MenuItem::from(item_ptr))
    }

    /// Inserts a separator at a specific position.
    pub fn insert_separator(&self, pos: usize) -> Option<MenuItem> {
        let item_ptr = unsafe { ffi::wxd_Menu_InsertSeparator(self.ptr, pos) };
        if item_ptr.is_null() {
            None
        } else {
            Some(MenuItem::from_ptr(item_ptr))
        }
    }

    /// Prepends a menu item.
    pub fn prepend(&self, id: Id, item: &str, help_string: &str, kind: ItemKind) -> Option<MenuItem> {
        let item_c = CString::new(item).unwrap_or_default();
        let help_c = CString::new(help_string).unwrap_or_default();
        let item_ptr = unsafe { ffi::wxd_Menu_Prepend(self.ptr, id, item_c.as_ptr(), help_c.as_ptr(), kind.into()) };
        if item_ptr.is_null() {
            None
        } else {
            Some(MenuItem::from_ptr(item_ptr))
        }
    }

    /// Prepends a submenu.
    pub fn prepend_submenu(&self, submenu: Menu, title: &str, help_string: &str) -> Option<MenuItem> {
        let title = CString::new(title).unwrap_or_default();
        let help_c = CString::new(help_string).unwrap_or_default();
        let item_ptr = unsafe { ffi::wxd_Menu_PrependSubMenu(self.ptr, submenu.into_raw_mut(), title.as_ptr(), help_c.as_ptr()) };
        if item_ptr.is_null() {
            return None;
        }
        Some(MenuItem::from(item_ptr))
    }

    /// Prepends a separator.
    pub fn prepend_separator(&self) -> Option<MenuItem> {
        let item_ptr = unsafe { ffi::wxd_Menu_PrependSeparator(self.ptr) };
        if item_ptr.is_null() {
            None
        } else {
            Some(MenuItem::from_ptr(item_ptr))
        }
    }

    /// Removes a menu item by ID. The item is not destroyed, but returned.
    /// Ownership of the returned MenuItem is transferred to the caller.
    pub fn remove(&self, id: Id) -> Option<MenuItem> {
        let ptr = unsafe { ffi::wxd_Menu_Remove(self.ptr, id) };
        if ptr.is_null() { None } else { Some(MenuItem::from(ptr)) }
    }

    /// Removes a menu item. The item is not destroyed, but returned.
    /// Ownership of the returned MenuItem is transferred to the caller.
    pub fn remove_item(&self, item: &MenuItem) -> Option<MenuItem> {
        let raw_item = item.as_const_ptr() as *mut ffi::wxd_MenuItem_t;
        let ptr = unsafe { ffi::wxd_Menu_RemoveItem(self.ptr, raw_item) };
        if ptr.is_null() { None } else { Some(MenuItem::from(ptr)) }
    }

    /// Deletes a menu item by ID.
    pub fn delete(&self, id: Id) -> bool {
        unsafe { ffi::wxd_Menu_Delete(self.ptr, id) }
    }

    /// Deletes a menu item.
    ///
    /// # Safety
    /// This method destroys the underlying C++ menu item. Any `MenuItem` wrappers
    /// pointing to this item (including the one passed in) will become invalid
    /// and must not be used.
    pub fn delete_item(&self, item: &MenuItem) -> bool {
        unsafe { ffi::wxd_Menu_DeleteItem(self.ptr, item.as_const_ptr() as *mut _) }
    }

    /// Finds an item by its position.
    pub fn find_item_by_position(&self, pos: usize) -> Option<MenuItem> {
        let ptr = unsafe { ffi::wxd_Menu_FindItemByPosition(self.ptr, pos) };
        if ptr.is_null() { None } else { Some(MenuItem::from_ptr(ptr)) }
    }

    /// Gets the help string associated with the given item ID.
    pub fn get_help_string(&self, id: Id) -> String {
        let len = unsafe { ffi::wxd_Menu_GetHelpString(self.ptr, id, std::ptr::null_mut(), 0) };
        if len < 0 {
            return String::new();
        }
        let mut buffer = vec![0u8; len as usize + 1];
        unsafe { ffi::wxd_Menu_GetHelpString(self.ptr, id, buffer.as_mut_ptr() as *mut _, buffer.len()) };
        unsafe { CStr::from_ptr(buffer.as_ptr() as *const _).to_string_lossy().into_owned() }
    }

    /// Sets the help string associated with the given item ID.
    pub fn set_help_string(&self, id: Id, help_string: &str) {
        let c_help = CString::new(help_string).unwrap_or_default();
        unsafe { ffi::wxd_Menu_SetHelpString(self.ptr, id, c_help.as_ptr()) }
    }

    /// Updates the UI of the menu items.
    /// The source argument is usually the window that handles the update events.
    /// If source is None, the menu itself or its invoker is used.
    pub fn update_ui(&self, source: Option<&impl crate::event::WxEvtHandler>) {
        let source_ptr = source
            .map(|s| unsafe { s.get_event_handler_ptr() })
            .unwrap_or(std::ptr::null_mut());
        unsafe { ffi::wxd_Menu_UpdateUI(self.ptr, source_ptr) }
    }

    /// Inserts a break in the menu.
    pub fn insert_break(&self) {
        unsafe { ffi::wxd_Menu_Break(self.ptr) }
    }

    /// Returns a list of all menu items in this menu.
    /// This is an iterative wrapper around FindItemByPosition.
    pub fn get_menu_items(&self) -> Vec<MenuItem> {
        let count = self.get_item_count();
        let mut items = Vec::with_capacity(count);
        for i in 0..count {
            if let Some(item) = self.find_item_by_position(i) {
                items.push(item);
            }
        }
        items
    }

    /// Gets a menu item by its XRC name.
    /// Returns a MenuItem wrapper that can be used for event binding.
    #[cfg(feature = "xrc")]
    pub fn get_item_by_name(&self, parent_handle: WindowHandle, item_name: &str) -> Option<MenuItem> {
        MenuItem::from_xrc_name(parent_handle, item_name)
    }

    // Make append private as it's called by builder
    fn append_raw(&self, id: Id, item: &str, help_string: &str, kind: ItemKind) -> Option<MenuItem> {
        let item_c = CString::new(item).unwrap_or_default();
        let help_c = CString::new(help_string).unwrap_or_default();
        let item_ptr = unsafe { ffi::wxd_Menu_Append(self.ptr, id, item_c.as_ptr(), help_c.as_ptr(), kind.into()) };
        if item_ptr.is_null() {
            None
        } else {
            // Return a MenuItem wrapper, but don't give it ownership
            Some(MenuItem::from_ptr(item_ptr))
        }
    }

    // Make append_separator private as it's called by builder
    fn append_separator_raw(&self) {
        unsafe { ffi::wxd_Menu_AppendSeparator(self.ptr) };
    }
}

// Note: No Drop impl here, as wxMenuBar takes ownership via Append.

// --- Menu Builder ---

// Enum to represent actions to perform on the menu during build
enum MenuAction {
    AppendItem {
        id: Id,
        item: String,
        help: String,
        kind: ItemKind,
    },
    AppendSeparator,
}

/// Builder for [`Menu`].
#[derive(Default)]
pub struct MenuBuilder {
    actions: Vec<MenuAction>,
    title: String,
    _marker: PhantomData<()>,
}

impl MenuBuilder {
    /// Creates a new, default builder.
    pub fn new() -> Self {
        Default::default()
    }

    pub fn with_title(mut self, title: &str) -> Self {
        self.title = title.to_string();
        self
    }

    /// Adds an item to be appended to the menu.
    pub fn append_item(mut self, id: Id, item: &str, help: &str) -> Self {
        self.actions.push(MenuAction::AppendItem {
            id,
            item: item.to_string(),
            help: help.to_string(),
            kind: ItemKind::Normal,
        });
        self
    }

    /// Adds a check item to be appended to the menu.
    pub fn append_check_item(mut self, id: Id, item: &str, help: &str) -> Self {
        self.actions.push(MenuAction::AppendItem {
            id,
            item: item.to_string(),
            help: help.to_string(),
            kind: ItemKind::Check,
        });
        self
    }

    /// Adds a radio item to be appended to the menu.
    pub fn append_radio_item(mut self, id: Id, item: &str, help: &str) -> Self {
        self.actions.push(MenuAction::AppendItem {
            id,
            item: item.to_string(),
            help: help.to_string(),
            kind: ItemKind::Radio,
        });
        self
    }

    /// Adds a separator to be appended to the menu.
    pub fn append_separator(mut self) -> Self {
        self.actions.push(MenuAction::AppendSeparator);
        self
    }

    /// Builds the `Menu`.
    ///
    /// # Panics
    /// Panics if the menu cannot be created.
    pub fn build(self) -> Menu {
        // Pass default style (0)
        let title_c = CString::new(self.title).unwrap();
        let style = 0i64;
        let ptr = unsafe { ffi::wxd_Menu_Create(title_c.as_ptr(), style as ffi::wxd_Style_t) };
        if ptr.is_null() {
            panic!("Failed to create Menu");
        }
        let menu = Menu {
            ptr,
            owned: true,
            _nosend_nosync: PhantomData,
        };

        // Perform actions
        for action in self.actions {
            match action {
                MenuAction::AppendItem { id, item, help, kind } => {
                    // We might ignore the returned MenuItem here, as the builder doesn't expose it.
                    let _ = menu.append_raw(id, &item, &help, kind);
                }
                MenuAction::AppendSeparator => {
                    menu.append_separator_raw();
                }
            }
        }
        menu
    }
}

// Add XRC support
#[cfg(feature = "xrc")]
impl crate::xrc::XrcSupport for Menu {
    unsafe fn from_xrc_ptr(ptr: *mut wxdragon_sys::wxd_Window_t) -> Self {
        let ptr = ptr as *mut wxdragon_sys::wxd_Menu_t;
        // Menus loaded via XRC are owned by their parent (e.g., MenuBar/Frame),
        // so this wrapper must NOT destroy them on drop.
        Self {
            ptr,
            owned: false,
            _nosend_nosync: PhantomData,
        }
    }
}

// Implement WxWidget for Menu (needed for XRC support)
impl crate::window::WxWidget for Menu {
    fn handle_ptr(&self) -> *mut wxdragon_sys::wxd_Window_t {
        self.ptr as *mut wxdragon_sys::wxd_Window_t
    }

    fn get_id(&self) -> i32 {
        -1 // Menus don't typically have IDs
    }
}

// --- Menu specific event enum ---
/// Events specific to Menu controls
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MenuEvent {
    /// Fired when an item is selected
    Selected,
}

/// Event data for Menu events
#[derive(Debug)]
pub struct MenuEventData {
    pub event: CommandEventData,
}

impl MenuEventData {
    pub fn new(event: Event) -> Self {
        Self {
            event: CommandEventData::new(event),
        }
    }

    /// Get the widget ID that generated the event
    pub fn get_id(&self) -> i32 {
        self.event.get_id()
    }
}

impl crate::event::WxEvtHandler for Menu {
    unsafe fn get_event_handler_ptr(&self) -> *mut wxdragon_sys::wxd_EvtHandler_t {
        self.ptr as *mut ffi::wxd_EvtHandler_t
    }
}

// At the bottom of the file, use the local macro
crate::implement_widget_local_event_handlers!(
    Menu,
    MenuEvent,
    MenuEventData,
    Selected => selected, EventType::MENU
);