winio_ui_app_kit/widgets/
combo_box.rs1use inherit_methods_macro::inherit_methods;
2use objc2::{
3 DeclaredClass, MainThreadOnly, define_class, msg_send,
4 rc::{Allocated, Retained},
5 runtime::ProtocolObject,
6};
7use objc2_app_kit::{
8 NSComboBox, NSComboBoxDelegate, NSControlTextEditingDelegate, NSTextFieldDelegate,
9};
10use objc2_foundation::{
11 MainThreadMarker, NSNotification, NSObject, NSObjectProtocol, NSString, ns_string,
12};
13use winio_callback::Callback;
14use winio_handle::AsContainer;
15use winio_primitive::{Point, Size};
16
17use crate::{GlobalRuntime, Result, Widget, catch, from_nsstring};
18
19#[derive(Debug)]
20pub struct ComboBox {
21 handle: Widget,
22 view: Retained<NSComboBox>,
23 delegate: Retained<ComboBoxDelegate>,
24}
25
26#[inherit_methods(from = "self.handle")]
27impl ComboBox {
28 pub fn new(parent: impl AsContainer) -> Result<Self> {
29 let parent = parent.as_container();
30 let mtm = parent.as_app_kit().mtm();
31
32 catch(|| unsafe {
33 let view = NSComboBox::new(mtm);
34 view.setBezeled(true);
35 view.setDrawsBackground(false);
36 view.setEditable(false);
37 view.setSelectable(false);
38 let handle = Widget::from_nsview(parent, Retained::cast_unchecked(view.clone()))?;
39
40 let delegate = ComboBoxDelegate::new(mtm);
41 let del_obj = ProtocolObject::from_ref(&*delegate);
42 view.setDelegate(Some(del_obj));
43
44 Ok(Self {
45 handle,
46 view,
47 delegate,
48 })
49 })
50 .flatten()
51 }
52
53 pub fn is_visible(&self) -> Result<bool>;
54
55 pub fn set_visible(&mut self, v: bool) -> Result<()>;
56
57 pub fn is_enabled(&self) -> Result<bool>;
58
59 pub fn set_enabled(&mut self, v: bool) -> Result<()>;
60
61 pub fn preferred_size(&self) -> Result<Size>;
62
63 pub fn loc(&self) -> Result<Point>;
64
65 pub fn set_loc(&mut self, p: Point) -> Result<()>;
66
67 pub fn size(&self) -> Result<Size>;
68
69 pub fn set_size(&mut self, v: Size) -> Result<()>;
70
71 pub fn tooltip(&self) -> Result<String>;
72
73 pub fn set_tooltip(&mut self, s: impl AsRef<str>) -> Result<()>;
74
75 pub fn text(&self) -> Result<String>;
76
77 pub fn set_text(&mut self, s: impl AsRef<str>) -> Result<()>;
78
79 pub fn selection(&self) -> Result<Option<usize>> {
80 catch(|| {
81 let index = self.view.indexOfSelectedItem();
82 if index < 0 { None } else { Some(index as _) }
83 })
84 }
85
86 pub fn set_selection(&mut self, i: usize) -> Result<()> {
87 let old_sel = self.selection()?;
88 if Some(i) != old_sel {
89 if let Some(i) = self.selection()? {
90 catch(|| self.view.deselectItemAtIndex(i as _))?;
91 }
92 catch(|| self.view.selectItemAtIndex(i as _))?;
93 }
94 Ok(())
95 }
96
97 pub fn is_editable(&self) -> Result<bool> {
98 catch(|| self.view.isEditable())
99 }
100
101 pub fn set_editable(&mut self, v: bool) -> Result<()> {
102 catch(|| {
103 self.view.setDrawsBackground(v);
104 self.view.setEditable(v);
105 self.view.setSelectable(v);
106 })
107 }
108
109 pub async fn wait_change(&self) {
110 self.delegate.ivars().changed.wait().await
111 }
112
113 pub async fn wait_select(&self) {
114 self.delegate.ivars().select.wait().await
115 }
116
117 pub fn len(&self) -> Result<usize> {
118 catch(|| self.view.numberOfItems() as _)
119 }
120
121 pub fn is_empty(&self) -> Result<bool> {
122 Ok(self.len()? == 0)
123 }
124
125 pub fn clear(&mut self) -> Result<()> {
126 catch(|| self.view.removeAllItems())
127 }
128
129 pub fn get(&self, i: usize) -> Result<String> {
130 catch(|| unsafe {
131 let s = Retained::cast_unchecked(self.view.itemObjectValueAtIndex(i as _));
132 from_nsstring(&s)
133 })
134 }
135
136 pub fn set(&mut self, i: usize, s: impl AsRef<str>) -> Result<()> {
137 self.remove(i)?;
138 self.insert(i, s)
139 }
140
141 pub fn insert(&mut self, i: usize, s: impl AsRef<str>) -> Result<()> {
142 catch(|| unsafe {
143 let s = NSString::from_str(s.as_ref());
144 self.view.insertItemWithObjectValue_atIndex(&s, i as _);
145 })?;
146 if (!self.is_editable()?) && self.len()? == 1 {
147 self.set_selection(0)?;
148 }
149 Ok(())
150 }
151
152 pub fn remove(&mut self, i: usize) -> Result<()> {
153 let i = i as isize;
154 catch(|| {
155 let remove_current = self.view.indexOfSelectedItem() == i;
156 self.view.removeItemAtIndex(i);
157 let len = self.view.numberOfItems();
158 if remove_current && (!self.is_editable()?) {
159 if len > 0 {
160 self.view.selectItemAtIndex(i.min(len - 1));
161 } else {
162 self.view.setStringValue(ns_string!(""));
163 }
164 }
165 Ok(())
166 })
167 .flatten()
168 }
169}
170
171winio_handle::impl_as_widget!(ComboBox, handle);
172
173#[derive(Debug, Default)]
174struct ComboBoxDelegateIvars {
175 changed: Callback,
176 select: Callback,
177}
178
179define_class! {
180 #[unsafe(super(NSObject))]
181 #[name = "WinioComboBoxDelegate"]
182 #[ivars = ComboBoxDelegateIvars]
183 #[thread_kind = MainThreadOnly]
184 #[derive(Debug)]
185 struct ComboBoxDelegate;
186
187 #[allow(non_snake_case)]
188 impl ComboBoxDelegate {
189 #[unsafe(method_id(init))]
190 fn init(this: Allocated<Self>) -> Option<Retained<Self>> {
191 let this = this.set_ivars(ComboBoxDelegateIvars::default());
192 unsafe { msg_send![super(this), init] }
193 }
194 }
195
196 unsafe impl NSObjectProtocol for ComboBoxDelegate {}
197
198 #[allow(non_snake_case)]
199 unsafe impl NSControlTextEditingDelegate for ComboBoxDelegate {
200 #[unsafe(method(controlTextDidChange:))]
201 fn controlTextDidChange(&self, _notification: &NSNotification) {
202 self.ivars().changed.signal::<GlobalRuntime>(());
203 }
204 }
205
206 unsafe impl NSTextFieldDelegate for ComboBoxDelegate {}
207
208 #[allow(non_snake_case)]
209 unsafe impl NSComboBoxDelegate for ComboBoxDelegate {
210 #[unsafe(method(comboBoxSelectionDidChange:))]
211 unsafe fn comboBoxSelectionDidChange(&self, _notification: &NSNotification) {
212 self.ivars().select.signal::<GlobalRuntime>(());
213 }
214 }
215}
216
217impl ComboBoxDelegate {
218 pub fn new(mtm: MainThreadMarker) -> Retained<Self> {
219 unsafe { msg_send![mtm.alloc::<Self>(), init] }
220 }
221}