native_windows_gui2/controls/
combo_box.rs

1use super::{ControlBase, ControlHandle};
2use crate::win32::base_helper::{check_hwnd, from_utf16, to_utf16};
3use crate::win32::window_helper as wh;
4use crate::{Font, NwgError, RawEventHandler, VTextAlign, unbind_raw_event_handler};
5use std::cell::{Ref, RefCell, RefMut};
6use std::fmt::Display;
7use std::mem;
8use winapi::shared::minwindef::{LPARAM, WPARAM};
9use winapi::shared::windef::HWND;
10use winapi::um::winuser::{WS_DISABLED, WS_TABSTOP, WS_VISIBLE};
11
12const NOT_BOUND: &'static str = "Combobox is not yet bound to a winapi object";
13const BAD_HANDLE: &'static str = "INTERNAL ERROR: Combobox handle is not HWND!";
14
15bitflags! {
16    /**
17        The ComboBox flags
18
19        * NONE:     No flags. Equivalent to a invisible combobox.
20        * VISIBLE:  The combobox is immediatly visible after creation
21        * DISABLED: The combobox cannot be interacted with by the user. It also has a grayed out look.
22        * TAB_STOP: The control can be selected using tab navigation
23    */
24    pub struct ComboBoxFlags: u32 {
25        const NONE = 0;
26        const VISIBLE = WS_VISIBLE;
27        const DISABLED = WS_DISABLED;
28        const TAB_STOP = WS_TABSTOP;
29    }
30}
31
32/**
33A combo box consists of a list and a selection field. The list presents the options that a user can select,
34and the selection field displays the current selection.
35
36Requires the `combobox` feature.
37
38**Builder parameters:**
39  * `parent`:         **Required.** The combobox parent container.
40  * `size`:           The combobox size.
41  * `position`:       The combobox position.
42  * `enabled`:        If the combobox can be used by the user. It also has a grayed out look if disabled.
43  * `flags`:          A combination of the ComboBoxFlags values.
44  * `ex_flags`:       A combination of win32 window extended flags. Unlike `flags`, ex_flags must be used straight from winapi
45  * `font`:           The font used for the combobox text
46  * `collection`:     The default collection of the combobox
47  * `selected_index`: The default selected index. None means no values are selected.
48  * `focus`:          The control receive focus after being created
49
50**Control events:**
51  * `OnComboBoxClosed`: When the combobox dropdown is closed
52  * `OnComboBoxDropdown`: When the combobox dropdown is opened
53  * `OnComboxBoxSelection`: When a new value in a combobox is choosen
54  * `MousePress(_)`: Generic mouse press events on the checkbox
55  * `OnMouseMove`: Generic mouse mouse event
56  * `OnMouseWheel`: Generic mouse wheel event
57
58
59```rust
60use native_windows_gui2 as nwg;
61fn build_combobox(combo: &mut nwg::ComboBox<&'static str>, window: &nwg::Window) {
62    let data = vec!["one", "two"];
63
64    nwg::ComboBox::builder()
65        .size((200, 300))
66        .collection(data)
67        .selected_index(Some(0))
68        .parent(window)
69        .build(combo);
70}
71```
72*/
73#[derive(Default)]
74pub struct ComboBox<D: Display + Default> {
75    pub handle: ControlHandle,
76    collection: RefCell<Vec<D>>,
77    handler0: RefCell<Option<RawEventHandler>>,
78}
79
80impl<D: Display + Default> ComboBox<D> {
81    pub fn builder<'a>() -> ComboBoxBuilder<'a, D> {
82        ComboBoxBuilder {
83            size: (100, 25),
84            position: (0, 0),
85            enabled: true,
86            focus: false,
87            flags: None,
88            ex_flags: 0,
89            font: None,
90            collection: None,
91            selected_index: None,
92            parent: None,
93        }
94    }
95
96    /// Remove the item at the selected index and returns it.
97    /// Panic of the index is out of bounds
98    pub fn remove(&self, index: usize) -> D {
99        use winapi::um::winuser::CB_DELETESTRING;
100
101        let handle = check_hwnd(&self.handle, NOT_BOUND, BAD_HANDLE);
102
103        wh::send_message(handle, CB_DELETESTRING, index as WPARAM, 0);
104
105        let mut col_ref = self.collection.borrow_mut();
106        col_ref.remove(index)
107    }
108
109    /// Sort the inner collection by the display value of it's items and update the view
110    /// Internally this uses `Vec.sort_unstable_by`.
111    pub fn sort(&self) {
112        use winapi::um::winuser::CB_ADDSTRING;
113
114        let handle = check_hwnd(&self.handle, NOT_BOUND, BAD_HANDLE);
115
116        self.clear_inner(handle);
117
118        let mut col = self.collection.borrow_mut();
119        col.sort_unstable_by(|a, b| {
120            let astr = format!("{}", a);
121            let bstr = format!("{}", b);
122            astr.cmp(&bstr)
123        });
124
125        for item in col.iter() {
126            let display = format!("{}", item);
127            let display_os = to_utf16(&display);
128            wh::send_message(handle, CB_ADDSTRING, 0, display_os.as_ptr() as LPARAM);
129        }
130    }
131
132    /// Show or hide the dropdown of the combox
133    pub fn dropdown(&self, v: bool) {
134        use winapi::um::winuser::CB_SHOWDROPDOWN;
135
136        let handle = check_hwnd(&self.handle, NOT_BOUND, BAD_HANDLE);
137        wh::send_message(handle, CB_SHOWDROPDOWN, v as usize, 0);
138    }
139
140    /// Return the index of the currencty selected item. Return `None` if no item is selected.
141    pub fn selection(&self) -> Option<usize> {
142        use winapi::um::winuser::{CB_ERR, CB_GETCURSEL};
143
144        let handle = check_hwnd(&self.handle, NOT_BOUND, BAD_HANDLE);
145
146        let index = wh::send_message(handle, CB_GETCURSEL, 0, 0);
147
148        if index == CB_ERR {
149            None
150        } else {
151            Some(index as usize)
152        }
153    }
154
155    /// Return the display value of the currenctly selected item
156    /// Return `None` if no item is selected. This reads the visual value.
157    pub fn selection_string(&self) -> Option<String> {
158        use winapi::shared::ntdef::WCHAR;
159        use winapi::um::winuser::{CB_ERR, CB_GETCURSEL, CB_GETLBTEXT, CB_GETLBTEXTLEN};
160
161        let handle = check_hwnd(&self.handle, NOT_BOUND, BAD_HANDLE);
162
163        let index = wh::send_message(handle, CB_GETCURSEL, 0, 0);
164
165        if index == CB_ERR {
166            None
167        } else {
168            let index = index as usize;
169            let length = (wh::send_message(handle, CB_GETLBTEXTLEN, index, 0) as usize) + 1; // +1 for the null character
170            let mut buffer: Vec<WCHAR> = Vec::with_capacity(length);
171            unsafe {
172                buffer.set_len(length);
173                wh::send_message(handle, CB_GETLBTEXT, index, buffer.as_ptr() as LPARAM);
174            }
175
176            Some(from_utf16(&buffer))
177        }
178    }
179
180    /// Set the currently selected item in the combobox.
181    /// Does nothing if the index is out of bound
182    /// If the value is None, remove the selected value
183    pub fn set_selection(&self, index: Option<usize>) {
184        use winapi::um::winuser::CB_SETCURSEL;
185
186        let handle = check_hwnd(&self.handle, NOT_BOUND, BAD_HANDLE);
187
188        let index = index.unwrap_or(-1isize as usize);
189        wh::send_message(handle, CB_SETCURSEL, index, 0);
190    }
191
192    /// Search an item that begins by the value and select the first one found.
193    /// The search is not case sensitive, so this string can contain any combination of uppercase and lowercase letters.
194    /// Return the index of the selected string or None if the search was not successful
195    pub fn set_selection_string(&self, value: &str) -> Option<usize> {
196        use winapi::um::winuser::{CB_ERR, CB_SELECTSTRING};
197
198        let handle = check_hwnd(&self.handle, NOT_BOUND, BAD_HANDLE);
199
200        let os_string = to_utf16(value);
201
202        let index = wh::send_message(handle, CB_SELECTSTRING, 0, os_string.as_ptr() as LPARAM);
203        if index == CB_ERR {
204            None
205        } else {
206            Some(index as usize)
207        }
208    }
209
210    /// Add a new item to the combobox. Sort the collection if the combobox is sorted.
211    pub fn push(&self, item: D) {
212        use winapi::um::winuser::CB_ADDSTRING;
213
214        let handle = check_hwnd(&self.handle, NOT_BOUND, BAD_HANDLE);
215
216        let display = format!("{}", item);
217        let display_os = to_utf16(&display);
218
219        wh::send_message(handle, CB_ADDSTRING, 0, display_os.as_ptr() as LPARAM);
220
221        self.collection.borrow_mut().push(item);
222    }
223
224    /// Insert an item in the collection and the control.
225    ///
226    /// SPECIAL behaviour! If index is `std::usize::MAX`, the item is added at the end of the collection.
227    /// The method will still panic if `index > len` with every other values.
228    pub fn insert(&self, index: usize, item: D) {
229        use winapi::um::winuser::CB_INSERTSTRING;
230
231        let handle = check_hwnd(&self.handle, NOT_BOUND, BAD_HANDLE);
232
233        let display = format!("{}", item);
234        let display_os = to_utf16(&display);
235
236        let mut col = self.collection.borrow_mut();
237        if index == std::usize::MAX {
238            col.push(item);
239        } else {
240            col.insert(index, item);
241        }
242
243        wh::send_message(
244            handle,
245            CB_INSERTSTRING,
246            index,
247            display_os.as_ptr() as LPARAM,
248        );
249    }
250
251    /// Update the visual of the control with the inner collection.
252    /// This rebuild every item in the combobox and can take some time on big collections.
253    pub fn sync(&self) {
254        use winapi::um::winuser::CB_ADDSTRING;
255
256        let handle = check_hwnd(&self.handle, NOT_BOUND, BAD_HANDLE);
257
258        self.clear_inner(handle);
259
260        for item in self.collection.borrow().iter() {
261            let display = format!("{}", item);
262            let display_os = to_utf16(&display);
263
264            wh::send_message(handle, CB_ADDSTRING, 0, display_os.as_ptr() as LPARAM);
265        }
266    }
267
268    /// Set the item collection of the combobox. Return the old collection
269    pub fn set_collection(&self, mut col: Vec<D>) -> Vec<D> {
270        use winapi::um::winuser::CB_ADDSTRING;
271
272        let handle = check_hwnd(&self.handle, NOT_BOUND, BAD_HANDLE);
273
274        self.clear_inner(handle);
275
276        for item in col.iter() {
277            let display = format!("{}", item);
278            let display_os = to_utf16(&display);
279            wh::send_message(handle, CB_ADDSTRING, 0, display_os.as_ptr() as LPARAM);
280        }
281
282        let mut col_ref = self.collection.borrow_mut();
283        mem::swap::<Vec<D>>(&mut col_ref, &mut col);
284
285        col
286    }
287
288    /// Return the number of items in the control. NOT the inner rust collection
289    pub fn len(&self) -> usize {
290        use winapi::um::winuser::CB_GETCOUNT;
291        let handle = check_hwnd(&self.handle, NOT_BOUND, BAD_HANDLE);
292        wh::send_message(handle, CB_GETCOUNT, 0, 0) as usize
293    }
294
295    //
296    // Common control functions
297    //
298
299    /// Return the font of the control
300    pub fn font(&self) -> Option<Font> {
301        let handle = check_hwnd(&self.handle, NOT_BOUND, BAD_HANDLE);
302
303        let font_handle = wh::get_window_font(handle);
304        if font_handle.is_null() {
305            None
306        } else {
307            Some(Font {
308                handle: font_handle,
309            })
310        }
311    }
312
313    /// Set the font of the control
314    pub fn set_font(&self, font: Option<&Font>) {
315        let handle = check_hwnd(&self.handle, NOT_BOUND, BAD_HANDLE);
316
317        wh::set_window_font(handle, font.map(|f| f.handle), true);
318    }
319
320    /// Return true if the control currently has the keyboard focus
321    pub fn focus(&self) -> bool {
322        let handle = check_hwnd(&self.handle, NOT_BOUND, BAD_HANDLE);
323        wh::get_focus(handle)
324    }
325
326    /// Set the keyboard focus on the button.
327    pub fn set_focus(&self) {
328        let handle = check_hwnd(&self.handle, NOT_BOUND, BAD_HANDLE);
329
330        wh::set_focus(handle);
331    }
332
333    /// Return true if the control user can interact with the control, return false otherwise
334    pub fn enabled(&self) -> bool {
335        let handle = check_hwnd(&self.handle, NOT_BOUND, BAD_HANDLE);
336        wh::get_window_enabled(handle)
337    }
338
339    /// Enable or disable the control
340    pub fn set_enabled(&self, v: bool) {
341        let handle = check_hwnd(&self.handle, NOT_BOUND, BAD_HANDLE);
342        wh::set_window_enabled(handle, v)
343    }
344
345    /// Return true if the control is visible to the user. Will return true even if the
346    /// control is outside of the parent client view (ex: at the position (10000, 10000))
347    pub fn visible(&self) -> bool {
348        let handle = check_hwnd(&self.handle, NOT_BOUND, BAD_HANDLE);
349        wh::get_window_visibility(handle)
350    }
351
352    /// Show or hide the control to the user
353    pub fn set_visible(&self, v: bool) {
354        let handle = check_hwnd(&self.handle, NOT_BOUND, BAD_HANDLE);
355        wh::set_window_visibility(handle, v)
356    }
357
358    /// Return the size of the button in the parent window
359    pub fn size(&self) -> (u32, u32) {
360        let handle = check_hwnd(&self.handle, NOT_BOUND, BAD_HANDLE);
361        wh::get_window_size(handle)
362    }
363
364    /// Set the size of the button in the parent window
365    pub fn set_size(&self, x: u32, y: u32) {
366        let handle = check_hwnd(&self.handle, NOT_BOUND, BAD_HANDLE);
367        wh::set_window_size(handle, x, y, false)
368    }
369
370    /// Return the position of the button in the parent window
371    pub fn position(&self) -> (i32, i32) {
372        let handle = check_hwnd(&self.handle, NOT_BOUND, BAD_HANDLE);
373        wh::get_window_position(handle)
374    }
375
376    /// Set the position of the button in the parent window
377    pub fn set_position(&self, x: i32, y: i32) {
378        let handle = check_hwnd(&self.handle, NOT_BOUND, BAD_HANDLE);
379        wh::set_window_position(handle, x, y)
380    }
381
382    /// Get read-only access to the inner collection of the combobox
383    /// This call refcell.borrow under the hood. Be sure to drop the value before
384    /// calling other combobox methods
385    pub fn collection(&self) -> Ref<'_, Vec<D>> {
386        self.collection.borrow()
387    }
388
389    /// Get mutable access to the inner collection of the combobox. Does not update the visual
390    /// control. Call `sync` to update the view. This call refcell.borrow_mut under the hood.
391    /// Be sure to drop the value before calling other combobox methods
392    pub fn collection_mut(&self) -> RefMut<'_, Vec<D>> {
393        self.collection.borrow_mut()
394    }
395
396    /// Winapi class name used during control creation
397    pub fn class_name(&self) -> &'static str {
398        "COMBOBOX"
399    }
400
401    /// Winapi base flags used during window creation
402    pub fn flags(&self) -> u32 {
403        WS_VISIBLE | WS_TABSTOP
404    }
405
406    /// Winapi flags required by the control
407    pub fn forced_flags(&self) -> u32 {
408        use winapi::um::winuser::{CBS_DROPDOWNLIST, WS_BORDER, WS_CHILD};
409        CBS_DROPDOWNLIST | WS_CHILD | WS_BORDER
410    }
411
412    /// Remove all value displayed in the control without touching the rust collection
413    fn clear_inner(&self, handle: HWND) {
414        use winapi::um::winuser::CB_RESETCONTENT;
415        wh::send_message(handle, CB_RESETCONTENT, 0, 0);
416    }
417
418    /// TODO: FIX VERTICAL CENTERING
419    #[allow(unused)]
420    fn hook_non_client_size(&self, bg: Option<[u8; 3]>, v_align: VTextAlign) {
421        use crate::bind_raw_event_handler_inner;
422        use std::ptr;
423        use winapi::shared::windef::{HBRUSH, HGDIOBJ, POINT, RECT};
424        use winapi::um::wingdi::{CreateSolidBrush, RGB, SelectObject};
425        use winapi::um::winuser::{
426            COLOR_WINDOW, DT_CALCRECT, DT_LEFT, NCCALCSIZE_PARAMS, WM_NCCALCSIZE, WM_NCPAINT,
427            WM_SIZE,
428        };
429        use winapi::um::winuser::{
430            DrawTextW, FillRect, GetClientRect, GetDC, GetWindowRect, ReleaseDC, ScreenToClient,
431            SetWindowPos,
432        };
433        use winapi::um::winuser::{SWP_FRAMECHANGED, SWP_NOMOVE, SWP_NOOWNERZORDER, SWP_NOSIZE};
434
435        if self.handle.blank() {
436            panic!("{}", NOT_BOUND);
437        }
438        let brush = match bg {
439            Some(c) => unsafe { CreateSolidBrush(RGB(c[0], c[1], c[2])) },
440            None => COLOR_WINDOW as HBRUSH,
441        };
442
443        unsafe {
444            let handler0 = bind_raw_event_handler_inner(&self.handle, 0, move |hwnd, msg, w, l| {
445                match msg {
446                    WM_NCCALCSIZE => {
447                        if w == 0 {
448                            return None;
449                        }
450
451                        // Calculate client area height needed for a font
452                        let font_handle = wh::get_window_font(hwnd);
453                        let mut r: RECT = mem::zeroed();
454                        let dc = GetDC(hwnd);
455
456                        let old = SelectObject(dc, font_handle as HGDIOBJ);
457
458                        let calc: [u16; 2] = [75, 121];
459                        DrawTextW(dc, calc.as_ptr(), 2, &mut r, DT_CALCRECT | DT_LEFT);
460
461                        let client_height = r.bottom - 5; // 5 is the combobox padding
462
463                        SelectObject(dc, old);
464                        ReleaseDC(hwnd, dc);
465
466                        // Calculate NC area to center text.
467                        let mut client: RECT = mem::zeroed();
468                        let mut window: RECT = mem::zeroed();
469                        GetClientRect(hwnd, &mut client);
470                        GetWindowRect(hwnd, &mut window);
471
472                        let window_height = window.bottom - window.top;
473                        let info_ptr: *mut NCCALCSIZE_PARAMS = l as *mut NCCALCSIZE_PARAMS;
474                        let info = &mut *info_ptr;
475                        match v_align {
476                            VTextAlign::Top => {
477                                info.rgrc[0].bottom -= window_height - client_height;
478                            }
479                            VTextAlign::Center => {
480                                let center = ((window_height - client_height) / 2) - 1;
481                                info.rgrc[0].top += center;
482                                info.rgrc[0].bottom -= center;
483                            }
484                            VTextAlign::Bottom => {
485                                info.rgrc[0].top += window_height - client_height;
486                            }
487                        }
488                    }
489                    WM_NCPAINT => {
490                        let mut window: RECT = mem::zeroed();
491                        let mut client: RECT = mem::zeroed();
492                        GetWindowRect(hwnd, &mut window);
493                        GetClientRect(hwnd, &mut client);
494
495                        let mut pt1 = POINT {
496                            x: window.left,
497                            y: window.top,
498                        };
499                        ScreenToClient(hwnd, &mut pt1);
500
501                        let mut pt2 = POINT {
502                            x: window.right,
503                            y: window.bottom,
504                        };
505                        ScreenToClient(hwnd, &mut pt2);
506
507                        let top = RECT {
508                            left: 0,
509                            top: pt1.y,
510                            right: client.right,
511                            bottom: client.top,
512                        };
513
514                        let bottom = RECT {
515                            left: 0,
516                            top: client.bottom,
517                            right: client.right,
518                            bottom: pt2.y,
519                        };
520
521                        let dc = GetDC(hwnd);
522                        FillRect(dc, &top, brush);
523                        FillRect(dc, &bottom, brush);
524                        ReleaseDC(hwnd, dc);
525                    }
526                    WM_SIZE => {
527                        SetWindowPos(
528                            hwnd,
529                            ptr::null_mut(),
530                            0,
531                            0,
532                            0,
533                            0,
534                            SWP_NOOWNERZORDER | SWP_NOSIZE | SWP_NOMOVE | SWP_FRAMECHANGED,
535                        );
536                    }
537                    _ => {}
538                }
539
540                None
541            });
542
543            *self.handler0.borrow_mut() = Some(handler0.unwrap());
544        }
545    }
546}
547
548impl<D: Display + Default> Drop for ComboBox<D> {
549    fn drop(&mut self) {
550        let handler = self.handler0.borrow();
551        if let Some(h) = handler.as_ref() {
552            drop(unbind_raw_event_handler(h));
553        }
554
555        self.handle.destroy();
556    }
557}
558
559pub struct ComboBoxBuilder<'a, D: Display + Default> {
560    size: (i32, i32),
561    position: (i32, i32),
562    enabled: bool,
563    focus: bool,
564    flags: Option<ComboBoxFlags>,
565    ex_flags: u32,
566    font: Option<&'a Font>,
567    collection: Option<Vec<D>>,
568    selected_index: Option<usize>,
569    parent: Option<ControlHandle>,
570}
571
572impl<'a, D: Display + Default> ComboBoxBuilder<'a, D> {
573    pub fn flags(mut self, flags: ComboBoxFlags) -> ComboBoxBuilder<'a, D> {
574        self.flags = Some(flags);
575        self
576    }
577
578    pub fn ex_flags(mut self, flags: u32) -> ComboBoxBuilder<'a, D> {
579        self.ex_flags = flags;
580        self
581    }
582
583    pub fn size(mut self, size: (i32, i32)) -> ComboBoxBuilder<'a, D> {
584        self.size = size;
585        self
586    }
587
588    pub fn position(mut self, pos: (i32, i32)) -> ComboBoxBuilder<'a, D> {
589        self.position = pos;
590        self
591    }
592
593    pub fn font(mut self, font: Option<&'a Font>) -> ComboBoxBuilder<'a, D> {
594        self.font = font;
595        self
596    }
597
598    pub fn parent<C: Into<ControlHandle>>(mut self, p: C) -> ComboBoxBuilder<'a, D> {
599        self.parent = Some(p.into());
600        self
601    }
602
603    pub fn collection(mut self, collection: Vec<D>) -> ComboBoxBuilder<'a, D> {
604        self.collection = Some(collection);
605        self
606    }
607
608    pub fn selected_index(mut self, index: Option<usize>) -> ComboBoxBuilder<'a, D> {
609        self.selected_index = index;
610        self
611    }
612
613    pub fn enabled(mut self, e: bool) -> ComboBoxBuilder<'a, D> {
614        self.enabled = e;
615        self
616    }
617
618    pub fn focus(mut self, focus: bool) -> ComboBoxBuilder<'a, D> {
619        self.focus = focus;
620        self
621    }
622
623    pub fn v_align(self, _align: VTextAlign) -> ComboBoxBuilder<'a, D> {
624        // Disabled for now because of a bug. Keep the method for backward compatibility
625        self
626    }
627
628    pub fn build(self, out: &mut ComboBox<D>) -> Result<(), NwgError> {
629        let flags = self.flags.map(|f| f.bits()).unwrap_or(out.flags());
630
631        let parent = match self.parent {
632            Some(p) => Ok(p),
633            None => Err(NwgError::no_parent("ComboBox")),
634        }?;
635
636        // Drop the old object
637        *out = ComboBox::default();
638
639        out.handle = ControlBase::build_hwnd()
640            .class_name(out.class_name())
641            .forced_flags(out.forced_flags())
642            .flags(flags)
643            .ex_flags(self.ex_flags)
644            .size(self.size)
645            .position(self.position)
646            .parent(Some(parent))
647            .build()?;
648
649        if self.font.is_some() {
650            out.set_font(self.font);
651        } else {
652            out.set_font(Font::global_default().as_ref());
653        }
654
655        if self.collection.is_some() {
656            out.set_collection(self.collection.unwrap());
657        }
658
659        if self.selected_index.is_some() {
660            out.set_selection(self.selected_index);
661        }
662
663        out.set_enabled(self.enabled);
664
665        if self.focus {
666            out.set_focus();
667        }
668
669        Ok(())
670    }
671}
672
673impl<D: Display + Default> PartialEq for ComboBox<D> {
674    fn eq(&self, other: &Self) -> bool {
675        self.handle == other.handle
676    }
677}