1use std::cell::Cell;
14use std::rc::Rc;
15use std::sync::Arc;
16
17use crate::color::Color;
18use crate::event::{Event, EventResult, Key, MouseButton};
19use crate::geometry::{Point, Rect, Size};
20use crate::draw_ctx::DrawCtx;
21use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
22use crate::text::Font;
23use crate::widget::{Widget, paint_subtree};
24use crate::widgets::label::Label;
25
26const CLOSED_H: f64 = 28.0;
27const ITEM_H: f64 = 24.0;
28const PAD_X: f64 = 8.0;
29const ARROW_W: f64 = 20.0;
30const CORNER_R: f64 = 4.0;
31
32pub struct ComboBox {
40 bounds: Rect,
41 children: Vec<Box<dyn Widget>>, base: WidgetBase,
43
44 options: Vec<String>,
45 selected: usize,
46 open: bool,
47 hovered_item: Option<usize>,
49
50 font: Arc<Font>,
51 font_size: f64,
52
53 on_change: Option<Box<dyn FnMut(usize)>>,
54 selected_cell: Option<Rc<Cell<usize>>>,
59
60 selected_label: Label,
63 item_labels: Vec<Label>,
65 item_fonts: Option<Vec<Arc<Font>>>,
71}
72
73impl ComboBox {
74 pub fn new(options: Vec<impl Into<String>>, selected: usize, font: Arc<Font>) -> Self {
79 let font_size = 13.0;
80 let opts: Vec<String> = options.into_iter().map(|s| s.into()).collect();
81 let sel = selected.min(opts.len().saturating_sub(1));
82
83 let selected_label = Self::make_label(
84 opts.get(sel).map(|s| s.as_str()).unwrap_or(""),
85 font_size,
86 Arc::clone(&font),
87 );
88 let item_labels = opts.iter().map(|t| {
89 Self::make_label(t, font_size, Arc::clone(&font))
90 }).collect();
91
92 Self {
93 bounds: Rect::default(),
94 children: Vec::new(),
95 base: WidgetBase::new(),
96 options: opts,
97 selected: sel,
98 open: false,
99 hovered_item: None,
100 font,
101 font_size,
102 on_change: None,
103 selected_cell: None,
104 selected_label,
105 item_labels,
106 item_fonts: None,
107 }
108 }
109
110 pub fn with_selected_cell(mut self, cell: Rc<Cell<usize>>) -> Self {
116 let n = self.options.len();
117 let v = cell.get();
118 if n > 0 {
119 let clamped = v.min(n - 1);
120 self.set_selected(clamped);
123 }
124 self.selected_cell = Some(cell);
125 self
126 }
127
128 fn make_label(text: &str, font_size: f64, font: Arc<Font>) -> Label {
129 Label::new(text, font)
130 .with_font_size(font_size)
131 }
132
133 pub fn with_font_size(mut self, size: f64) -> Self {
136 self.font_size = size;
137 self.selected_label = Self::make_label(
138 self.options.get(self.selected).map(|s| s.as_str()).unwrap_or(""),
139 size,
140 Arc::clone(&self.font),
141 );
142 self.item_labels = self.options.iter().map(|t| {
143 Self::make_label(t, size, Arc::clone(&self.font))
144 }).collect();
145 self
146 }
147
148 pub fn with_margin(mut self, m: Insets) -> Self { self.base.margin = m; self }
149 pub fn with_h_anchor(mut self, h: HAnchor) -> Self { self.base.h_anchor = h; self }
150 pub fn with_v_anchor(mut self, v: VAnchor) -> Self { self.base.v_anchor = v; self }
151 pub fn with_min_size(mut self, s: Size) -> Self { self.base.min_size = s; self }
152 pub fn with_max_size(mut self, s: Size) -> Self { self.base.max_size = s; self }
153
154 pub fn on_change(mut self, cb: impl FnMut(usize) + 'static) -> Self {
156 self.on_change = Some(Box::new(cb));
157 self
158 }
159
160 pub fn with_item_fonts(mut self, fonts: Vec<Arc<Font>>) -> Self {
173 self.item_fonts = Some(fonts.clone());
174 let size = self.font_size;
175 self.item_labels = self.options.iter().enumerate().map(|(i, t)| {
176 let f = fonts.get(i).cloned()
177 .unwrap_or_else(|| Arc::clone(&self.font));
178 Label::new(t, f)
179 .with_font_size(size)
180 .with_ignore_system_font(true)
181 }).collect();
182 if let Some(sel_font) = fonts.get(self.selected).cloned() {
184 self.selected_label = Label::new(
185 self.options.get(self.selected).map(|s| s.as_str()).unwrap_or(""),
186 sel_font,
187 )
188 .with_font_size(size)
189 .with_ignore_system_font(true);
190 }
191 self
192 }
193
194 pub fn selected(&self) -> usize { self.selected }
197
198 pub fn set_selected(&mut self, idx: usize) {
199 if idx < self.options.len() {
200 self.selected = idx;
201 if let Some(ref fonts) = self.item_fonts {
206 if let Some(f) = fonts.get(idx).cloned() {
207 self.selected_label = Label::new(
208 self.options[idx].as_str(), f,
209 )
210 .with_font_size(self.font_size)
211 .with_ignore_system_font(true);
212 return;
213 }
214 }
215 self.selected_label.set_text(self.options[idx].as_str());
216 }
217 }
218
219 fn fire(&mut self) {
222 let idx = self.selected;
223 if let Some(cell) = &self.selected_cell { cell.set(idx); }
224 if let Some(cb) = self.on_change.as_mut() { cb(idx); }
225 }
226
227 fn total_h(&self) -> f64 {
229 if self.open {
230 CLOSED_H + self.options.len() as f64 * ITEM_H
231 } else {
232 CLOSED_H
233 }
234 }
235
236 fn item_top_y(&self, i: usize) -> f64 {
242 let dropdown_h = self.total_h() - CLOSED_H;
245 dropdown_h - (i as f64 * ITEM_H)
246 }
247
248 fn item_rect(&self, i: usize) -> Rect {
249 let w = self.bounds.width;
250 let ty = self.item_top_y(i);
251 Rect::new(0.0, ty - ITEM_H, w, ITEM_H)
252 }
253
254 fn item_for_pos(&self, p: Point) -> Option<usize> {
256 if !self.open { return None; }
257 for i in 0..self.options.len() {
258 let r = self.item_rect(i);
259 if p.x >= r.x && p.x <= r.x + r.width
260 && p.y >= r.y && p.y <= r.y + r.height
261 {
262 return Some(i);
263 }
264 }
265 None
266 }
267
268 fn in_button(&self, p: Point) -> bool {
270 let button_y = self.total_h() - CLOSED_H;
271 p.x >= 0.0 && p.x <= self.bounds.width
272 && p.y >= button_y && p.y <= self.total_h()
273 }
274}
275
276impl Widget for ComboBox {
277 fn type_name(&self) -> &'static str { "ComboBox" }
278 fn bounds(&self) -> Rect { self.bounds }
279 fn set_bounds(&mut self, b: Rect) { self.bounds = b; }
280 fn children(&self) -> &[Box<dyn Widget>] { &self.children }
281 fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> { &mut self.children }
282
283 fn is_focusable(&self) -> bool { true }
284
285 fn margin(&self) -> Insets { self.base.margin }
286 fn h_anchor(&self) -> HAnchor { self.base.h_anchor }
287 fn v_anchor(&self) -> VAnchor { self.base.v_anchor }
288 fn min_size(&self) -> Size { self.base.min_size }
289 fn max_size(&self) -> Size { self.base.max_size }
290
291 fn layout(&mut self, available: Size) -> Size {
292 if !self.open {
297 if let Some(cell) = &self.selected_cell {
298 let n = self.options.len();
299 if n > 0 {
300 let v = cell.get().min(n - 1);
301 if v != self.selected {
302 self.set_selected(v);
305 }
306 }
307 }
308 }
309
310 let h = self.total_h();
311 self.bounds = Rect::new(0.0, 0.0, available.width, h);
312 let inner_w = (available.width - PAD_X * 2.0 - ARROW_W).max(0.0);
313
314 let sl = self.selected_label.layout(Size::new(inner_w, CLOSED_H));
316 let sl_y = (self.total_h() - CLOSED_H) + (CLOSED_H - sl.height) * 0.5;
317 self.selected_label.set_bounds(Rect::new(PAD_X, sl_y, sl.width, sl.height));
318
319 let dropdown_h = self.total_h() - CLOSED_H;
321 for i in 0..self.item_labels.len() {
322 let s = self.item_labels[i].layout(Size::new(inner_w, ITEM_H));
323 let ty = dropdown_h - (i as f64 * ITEM_H);
324 let ly = ty - ITEM_H + (ITEM_H - s.height) * 0.5;
325 self.item_labels[i].set_bounds(Rect::new(PAD_X, ly, s.width, s.height));
326 }
327
328 Size::new(available.width, h)
329 }
330
331 fn paint(&mut self, ctx: &mut dyn DrawCtx) {
332 let v = ctx.visuals();
333 let w = self.bounds.width;
334 let h = self.total_h();
335 let btn_y = h - CLOSED_H;
337
338 ctx.set_fill_color(v.widget_bg);
340 ctx.begin_path();
341 ctx.rounded_rect(0.0, btn_y, w, CLOSED_H, CORNER_R);
342 ctx.fill();
343
344 ctx.set_stroke_color(v.widget_stroke);
345 ctx.set_line_width(1.0);
346 ctx.begin_path();
347 ctx.rounded_rect(0.0, btn_y, w, CLOSED_H, CORNER_R);
348 ctx.stroke();
349
350 let arrow_x = w - ARROW_W * 0.5;
352 let arrow_cy = btn_y + CLOSED_H * 0.5;
353 let arrow_sz = 4.0;
354 ctx.set_fill_color(v.text_dim);
355 ctx.begin_path();
356 ctx.move_to(arrow_x - arrow_sz, arrow_cy + arrow_sz * 0.5);
358 ctx.line_to(arrow_x + arrow_sz, arrow_cy + arrow_sz * 0.5);
359 ctx.line_to(arrow_x, arrow_cy - arrow_sz * 0.5);
360 ctx.close_path();
361 ctx.fill();
362
363 self.selected_label.set_color(v.text_color);
365 let sl_bounds = self.selected_label.bounds();
366
367 ctx.save();
368 ctx.translate(sl_bounds.x, sl_bounds.y);
369 paint_subtree(&mut self.selected_label, ctx);
370 ctx.restore();
371
372 if self.open {
374 let dropdown_h = h - CLOSED_H;
375
376 ctx.set_fill_color(v.widget_bg);
378 ctx.begin_path();
379 ctx.rounded_rect(0.0, 0.0, w, dropdown_h, CORNER_R);
380 ctx.fill();
381
382 ctx.set_stroke_color(v.widget_stroke);
383 ctx.set_line_width(1.0);
384 ctx.begin_path();
385 ctx.rounded_rect(0.0, 0.0, w, dropdown_h, CORNER_R);
386 ctx.stroke();
387
388 for i in 0..self.options.len() {
390 let ir = self.item_rect(i);
391
392 let is_hovered = self.hovered_item == Some(i);
394 let is_selected = i == self.selected;
395 if is_selected || is_hovered {
396 let bg = if is_selected { v.accent } else { v.widget_bg_hovered };
397 ctx.set_fill_color(bg);
398 ctx.begin_path();
399 ctx.rounded_rect(2.0, ir.y + 1.0, w - 4.0, ir.height - 2.0, 3.0);
400 ctx.fill();
401 }
402
403 let text_color = if is_selected { Color::white() } else { v.text_color };
405 self.item_labels[i].set_color(text_color);
406 let lb = self.item_labels[i].bounds();
407
408 ctx.save();
409 ctx.translate(lb.x, lb.y);
410 paint_subtree(&mut self.item_labels[i], ctx);
411 ctx.restore();
412 }
413 }
414 }
415
416 fn on_event(&mut self, event: &Event) -> EventResult {
417 match event {
418 Event::MouseDown { button: MouseButton::Left, pos, .. } => {
419 if self.in_button(*pos) {
420 self.open = !self.open;
421 self.hovered_item = None;
422 crate::animation::request_tick();
423 return EventResult::Consumed;
424 }
425 if self.open {
426 if let Some(i) = self.item_for_pos(*pos) {
427 self.set_selected(i);
437 self.open = false;
438 self.hovered_item = None;
439 self.fire();
440 crate::animation::request_tick();
441 return EventResult::Consumed;
442 }
443 self.open = false;
445 self.hovered_item = None;
446 crate::animation::request_tick();
447 return EventResult::Consumed;
448 }
449 EventResult::Ignored
450 }
451 Event::MouseMove { pos } => {
452 self.hovered_item = self.item_for_pos(*pos);
453 EventResult::Ignored
454 }
455 Event::KeyDown { key, .. } => {
456 let n = self.options.len();
457 match key {
458 Key::Enter | Key::Char(' ') => {
459 self.open = !self.open;
460 crate::animation::request_tick();
461 EventResult::Consumed
462 }
463 Key::Escape => {
464 if self.open {
465 self.open = false;
466 crate::animation::request_tick();
467 EventResult::Consumed
468 } else {
469 EventResult::Ignored
470 }
471 }
472 Key::ArrowDown => {
473 if self.selected + 1 < n {
474 self.selected += 1;
475 self.selected_label.set_text(self.options[self.selected].as_str());
476 self.fire();
477 crate::animation::request_tick();
478 }
479 EventResult::Consumed
480 }
481 Key::ArrowUp => {
482 if self.selected > 0 {
483 self.selected -= 1;
484 self.selected_label.set_text(self.options[self.selected].as_str());
485 self.fire();
486 crate::animation::request_tick();
487 }
488 EventResult::Consumed
489 }
490 _ => EventResult::Ignored,
491 }
492 }
493 Event::FocusLost => {
494 let was_open = self.open;
495 self.open = false;
496 self.hovered_item = None;
497 if was_open { crate::animation::request_tick(); }
498 EventResult::Ignored
499 }
500 _ => EventResult::Ignored,
501 }
502 }
503
504 fn properties(&self) -> Vec<(&'static str, String)> {
505 vec![
506 ("selected", self.selected.to_string()),
507 ("open", self.open.to_string()),
508 ("options", self.options.len().to_string()),
509 ]
510 }
511}