1use std::f32::consts::PI;
4
5use crate::interaction::InteractionState;
6use crate::layout::{
7 DROPDOWN_BOX_HEIGHT, GRID_GAP, GRID_PADDING, GRID_SECTION_H, GridLayout, HEADER_HEIGHT, Layout,
8 PluginLayout, ROWS_COLUMN_GAP, ROWS_LAYOUT_TOP, ROWS_ROW_GAP, ROWS_SECTION_LABEL_HEIGHT,
9 WidgetKind, compute_section_offsets,
10};
11use crate::render::RenderBackend;
12use crate::snapshot::ParamSnapshot;
13use crate::theme::{Color, Theme};
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17pub enum WidgetType {
18 Knob,
19 Slider,
20 Toggle,
21 Selector,
22 Dropdown,
24 Meter,
25 XYPad,
26}
27
28pub fn draw_knob(
34 ctx: &mut dyn RenderBackend,
35 x: f32,
36 y: f32,
37 size: f32,
38 value: f32,
39 label: &str,
40 value_text: &str,
41 theme: &Theme,
42 highlighted: bool,
43) {
44 let cx = x + size / 2.0;
45 let cy = y + size / 2.0 - 5.0; let radius = size / 2.0 - 4.0;
47
48 let start_angle = 0.75 * PI; let end_angle = 2.25 * PI; let arc_start = start_angle;
53 let arc_end = end_angle;
54
55 ctx.stroke_arc(cx, cy, radius, arc_start, arc_end, theme.knob_track, 2.0);
57
58 let value_angle = arc_start + value * (arc_end - arc_start);
60 if value > 0.01 {
61 ctx.stroke_arc(cx, cy, radius, arc_start, value_angle, theme.knob_fill, 2.0);
62 }
63
64 let pointer_len = radius * 0.6;
66 let px = cx + pointer_len * value_angle.cos();
67 let py = cy + pointer_len * value_angle.sin();
68 ctx.draw_line(cx, cy, px, py, theme.knob_pointer, 1.5);
69
70 if highlighted {
72 ctx.stroke_arc(cx, cy, radius + 2.0, arc_start, arc_end, theme.accent, 1.0);
73 }
74
75 let val_size = 10.0;
77 let val_w = ctx.text_width(value_text, val_size);
78 ctx.draw_text(
79 value_text,
80 cx - val_w / 2.0,
81 y + size - 9.0,
82 val_size,
83 theme.text,
84 );
85
86 let label_size = 9.0;
88 let label_w = ctx.text_width(label, label_size);
89 ctx.draw_text(
90 label,
91 cx - label_w / 2.0,
92 y + size + 2.0,
93 label_size,
94 theme.text_dim,
95 );
96}
97
98pub fn draw_header(
102 ctx: &mut dyn RenderBackend,
103 x: f32,
104 y: f32,
105 w: f32,
106 h: f32,
107 title: Option<&str>,
108 subtitle: Option<&str>,
109 theme: &Theme,
110) {
111 ctx.fill_rect(x, y, w, h, theme.header_bg);
112
113 if let Some(title) = title {
114 let title_size = 12.0;
115 ctx.draw_text(
116 title,
117 x + 10.0,
118 y + (h - title_size) / 2.0 - 1.0,
119 title_size,
120 theme.header_text,
121 );
122 }
123
124 if let Some(subtitle) = subtitle {
125 let sub_size = 9.0;
126 let sub_w = ctx.text_width(subtitle, sub_size);
127 ctx.draw_text(
128 subtitle,
129 x + w - sub_w - 10.0,
130 y + (h - sub_size) / 2.0 - 1.0,
131 sub_size,
132 theme.text_dim,
133 );
134 }
135}
136
137pub fn draw_slider(
141 ctx: &mut dyn RenderBackend,
142 x: f32,
143 y: f32,
144 width: f32,
145 height: f32,
146 value: f32,
147 label: &str,
148 value_text: &str,
149 theme: &Theme,
150 highlighted: bool,
151) {
152 let track_y = y + height / 2.0 - 5.0;
153 let track_h = 3.0;
154 let margin = 4.0;
155 let track_w = width - margin * 2.0;
156
157 ctx.fill_rect(x + margin, track_y, track_w, track_h, theme.knob_track);
159
160 let fill_w = track_w * value;
162 if fill_w > 0.5 {
163 ctx.fill_rect(x + margin, track_y, fill_w, track_h, theme.knob_fill);
164 }
165
166 let thumb_x = x + margin + fill_w;
168 let thumb_r = 4.0;
169 ctx.fill_circle(
170 thumb_x,
171 track_y + track_h / 2.0,
172 thumb_r,
173 theme.knob_pointer,
174 );
175 if highlighted {
176 ctx.fill_circle(
177 thumb_x,
178 track_y + track_h / 2.0,
179 thumb_r + 1.5,
180 theme.accent,
181 );
182 ctx.fill_circle(
183 thumb_x,
184 track_y + track_h / 2.0,
185 thumb_r,
186 theme.knob_pointer,
187 );
188 }
189
190 let val_size = 10.0;
192 let cx = x + width / 2.0;
193 let val_w = ctx.text_width(value_text, val_size);
194 ctx.draw_text(
195 value_text,
196 cx - val_w / 2.0,
197 y + height - 9.0,
198 val_size,
199 theme.text,
200 );
201
202 let label_size = 9.0;
204 let label_w = ctx.text_width(label, label_size);
205 ctx.draw_text(
206 label,
207 cx - label_w / 2.0,
208 y + height + 2.0,
209 label_size,
210 theme.text_dim,
211 );
212}
213
214pub fn draw_toggle(
218 ctx: &mut dyn RenderBackend,
219 x: f32,
220 y: f32,
221 width: f32,
222 height: f32,
223 value: f32,
224 label: &str,
225 value_text: &str,
226 theme: &Theme,
227 highlighted: bool,
228) {
229 let is_on = value > 0.5;
230 let cx = x + width / 2.0;
231 let cy = y + height / 2.0 - 5.0;
232
233 let track_w = 20.0;
235 let track_h = 10.0;
236 let track_x = cx - track_w / 2.0;
237 let track_y = cy - track_h / 2.0;
238 let bg = if is_on {
239 theme.knob_fill
240 } else {
241 theme.knob_track
242 };
243 ctx.fill_rect(track_x, track_y, track_w, track_h, bg);
244
245 let thumb_x = if is_on {
247 track_x + track_w - track_h / 2.0
248 } else {
249 track_x + track_h / 2.0
250 };
251 ctx.fill_circle(thumb_x, cy, track_h / 2.0 - 1.0, theme.knob_pointer);
252
253 if highlighted {
254 ctx.fill_rect(
255 track_x - 1.0,
256 track_y - 1.0,
257 track_w + 2.0,
258 track_h + 2.0,
259 theme.accent,
260 );
261 ctx.fill_rect(track_x, track_y, track_w, track_h, bg);
262 ctx.fill_circle(thumb_x, cy, track_h / 2.0 - 1.0, theme.knob_pointer);
263 }
264
265 let val_size = 10.0;
267 let val_w = ctx.text_width(value_text, val_size);
268 ctx.draw_text(
269 value_text,
270 cx - val_w / 2.0,
271 y + height - 9.0,
272 val_size,
273 theme.text,
274 );
275
276 let label_size = 9.0;
278 let label_w = ctx.text_width(label, label_size);
279 ctx.draw_text(
280 label,
281 cx - label_w / 2.0,
282 y + height + 2.0,
283 label_size,
284 theme.text_dim,
285 );
286}
287
288pub fn draw_selector(
292 ctx: &mut dyn RenderBackend,
293 x: f32,
294 y: f32,
295 width: f32,
296 height: f32,
297 _value: f32,
298 label: &str,
299 value_text: &str,
300 theme: &Theme,
301 highlighted: bool,
302) {
303 let cx = x + width / 2.0;
304 let cy = y + height / 2.0 - 5.0;
305
306 let val_size = 10.0;
308 let arrow_size = 8.0;
309 let arrow_pad = 9.0; let val_w = ctx.text_width(value_text, val_size);
311 let box_w = (val_w + arrow_pad * 2.0 + 5.0).max(width - 8.0);
312 let box_h = 13.0;
313 let box_x = cx - box_w / 2.0;
314 let box_y = cy - box_h / 2.0;
315 let bg = if highlighted {
316 theme.accent
317 } else {
318 theme.knob_track
319 };
320 ctx.fill_rect(box_x, box_y, box_w, box_h, bg);
321
322 ctx.draw_text(
324 value_text,
325 cx - val_w / 2.0,
326 cy - val_size / 2.0,
327 val_size,
328 theme.text,
329 );
330
331 ctx.draw_text(
333 "<",
334 box_x + 3.0,
335 cy - arrow_size / 2.0,
336 arrow_size,
337 theme.text_dim,
338 );
339 let gt_w = ctx.text_width(">", arrow_size);
340 ctx.draw_text(
341 ">",
342 box_x + box_w - gt_w - 3.0,
343 cy - arrow_size / 2.0,
344 arrow_size,
345 theme.text_dim,
346 );
347
348 let label_size = 9.0;
350 let label_w = ctx.text_width(label, label_size);
351 ctx.draw_text(
352 label,
353 cx - label_w / 2.0,
354 y + height + 2.0,
355 label_size,
356 theme.text_dim,
357 );
358}
359
360pub fn draw_dropdown(
364 ctx: &mut dyn RenderBackend,
365 x: f32,
366 y: f32,
367 width: f32,
368 height: f32,
369 _value: f32,
370 label: &str,
371 value_text: &str,
372 theme: &Theme,
373 highlighted: bool,
374 is_open: bool,
375) {
376 let cx = x + width / 2.0;
377 let cy = y + height / 2.0 - 8.0;
378
379 let val_size = 10.0;
380 let arrow_pad = 14.0;
381 let val_w = ctx.text_width(value_text, val_size);
382 let box_w = (val_w + arrow_pad + 12.0).max(width - 12.0);
383 let box_h = DROPDOWN_BOX_HEIGHT;
384 let box_x = cx - box_w / 2.0;
385 let box_y = cy - box_h / 2.0;
386 let bg = if is_open || highlighted {
387 theme.accent
388 } else {
389 theme.knob_track
390 };
391 ctx.fill_rect(box_x, box_y, box_w, box_h, bg);
392
393 ctx.draw_text(
395 value_text,
396 box_x + 6.0,
397 cy - val_size / 2.0,
398 val_size,
399 theme.text,
400 );
401
402 let arrow_size = 8.0;
404 let arrow = if is_open { "\u{25B2}" } else { "\u{25BC}" }; let aw = ctx.text_width(arrow, arrow_size);
406 ctx.draw_text(
407 arrow,
408 box_x + box_w - aw - 4.0,
409 cy - arrow_size / 2.0,
410 arrow_size,
411 theme.text_dim,
412 );
413
414 let label_size = 9.0;
416 let label_w = ctx.text_width(label, label_size);
417 ctx.draw_text(
418 label,
419 cx - label_w / 2.0,
420 y + height + 2.0,
421 label_size,
422 theme.text_dim,
423 );
424}
425
426#[allow(clippy::cast_precision_loss)]
433pub fn draw_dropdown_popup(
434 ctx: &mut dyn RenderBackend,
435 x: f32,
436 y: f32,
437 width: f32,
438 options: &[String],
439 selected_index: usize,
440 hover_index: Option<usize>,
441 scroll_offset: usize,
442 visible_count: usize,
443 theme: &Theme,
444) {
445 let item_h = 18.0;
446 let padding = 4.0;
447 let popup_w = width.max(80.0);
448 let popup_h = visible_count as f32 * item_h + padding * 2.0;
449 let popup_x = x;
450 let popup_y = y;
451
452 ctx.fill_rect(popup_x, popup_y, popup_w, popup_h, theme.surface);
454 ctx.draw_line(
456 popup_x,
457 popup_y,
458 popup_x + popup_w,
459 popup_y,
460 theme.text_dim,
461 1.0,
462 );
463 ctx.draw_line(
464 popup_x + popup_w,
465 popup_y,
466 popup_x + popup_w,
467 popup_y + popup_h,
468 theme.text_dim,
469 1.0,
470 );
471 ctx.draw_line(
472 popup_x + popup_w,
473 popup_y + popup_h,
474 popup_x,
475 popup_y + popup_h,
476 theme.text_dim,
477 1.0,
478 );
479 ctx.draw_line(
480 popup_x,
481 popup_y + popup_h,
482 popup_x,
483 popup_y,
484 theme.text_dim,
485 1.0,
486 );
487
488 let text_size = 10.0;
489 let visible_end = (scroll_offset + visible_count).min(options.len());
490 for (vis_i, abs_i) in (scroll_offset..visible_end).enumerate() {
491 let iy = popup_y + padding + vis_i as f32 * item_h;
492
493 if hover_index == Some(abs_i) {
495 ctx.fill_rect(popup_x + 1.0, iy, popup_w - 2.0, item_h, theme.accent);
496 } else if abs_i == selected_index {
497 ctx.fill_rect(popup_x + 1.0, iy, popup_w - 2.0, item_h, theme.knob_track);
498 }
499
500 ctx.draw_text(
501 &options[abs_i],
502 popup_x + 6.0,
503 iy + (item_h - text_size) / 2.0,
504 text_size,
505 theme.text,
506 );
507 }
508
509 let arrow_size = 8.0;
511 let cx = popup_x + popup_w / 2.0;
512 if scroll_offset > 0 {
513 let aw = ctx.text_width("\u{25B2}", arrow_size);
514 ctx.draw_text(
515 "\u{25B2}",
516 cx - aw / 2.0,
517 popup_y + 1.0,
518 arrow_size,
519 theme.text_dim,
520 );
521 }
522 if visible_end < options.len() {
523 let aw = ctx.text_width("\u{25BC}", arrow_size);
524 ctx.draw_text(
525 "\u{25BC}",
526 cx - aw / 2.0,
527 popup_y + popup_h - arrow_size - 1.0,
528 arrow_size,
529 theme.text_dim,
530 );
531 }
532}
533
534#[allow(clippy::cast_precision_loss)]
540pub fn draw_meter(
541 ctx: &mut dyn RenderBackend,
542 x: f32,
543 y: f32,
544 width: f32,
545 height: f32,
546 levels: &[f32],
547 label: &str,
548 theme: &Theme,
549) {
550 let cx = x + width / 2.0;
551 let num = levels.len().max(1);
552 let bar_w = 4.0f32;
553 let gap = 2.0f32;
554 let total_bar_w = num as f32 * bar_w + (num as f32 - 1.0).max(0.0) * gap;
555 let bar_h = height - 4.0; let bar_start_x = cx - total_bar_w / 2.0;
557 let bar_y = y + 2.0;
558
559 for (i, &level) in levels.iter().enumerate() {
560 let bx = bar_start_x + i as f32 * (bar_w + gap);
561
562 ctx.fill_rect(bx, bar_y, bar_w, bar_h, theme.knob_track);
564
565 let display = truce_core::meter_display(level);
567 let fill_h = bar_h * display;
568 if fill_h > 0.5 {
569 let color = if display > 0.95 {
571 Color::rgb(0.88, 0.27, 0.27)
572 } else {
573 theme.knob_fill
574 };
575 ctx.fill_rect(bx, bar_y + bar_h - fill_h, bar_w, fill_h, color);
576 }
577 }
578
579 let label_size = 8.0;
581 let label_w = ctx.text_width(label, label_size);
582 ctx.draw_text(
583 label,
584 cx - label_w / 2.0,
585 y + height + 4.0,
586 label_size,
587 theme.text_dim,
588 );
589}
590
591pub fn draw_xy_pad(
595 ctx: &mut dyn RenderBackend,
596 x: f32,
597 y: f32,
598 width: f32,
599 height: f32,
600 value_x: f32,
601 value_y: f32,
602 label_x: &str,
603 label_y: &str,
604 theme: &Theme,
605 highlighted: bool,
606) {
607 let pad_margin = 4.0;
608 let pad_x = x + pad_margin;
609 let pad_y = y + pad_margin;
610 let pad_w = width - pad_margin * 2.0;
611 let pad_h = height - pad_margin * 2.0;
612
613 ctx.fill_rect(pad_x, pad_y, pad_w, pad_h, theme.knob_track);
615
616 let dot_x = pad_x + value_x.clamp(0.0, 1.0) * pad_w;
618 let dot_y = pad_y + (1.0 - value_y.clamp(0.0, 1.0)) * pad_h; let line_color = theme.text_dim;
620 ctx.draw_line(dot_x, pad_y, dot_x, pad_y + pad_h, line_color, 1.0);
621 ctx.draw_line(pad_x, dot_y, pad_x + pad_w, dot_y, line_color, 1.0);
622
623 let dot_color = if highlighted {
625 theme.accent
626 } else {
627 theme.knob_fill
628 };
629 ctx.fill_circle(dot_x, dot_y, 3.0, dot_color);
630 ctx.fill_circle(dot_x, dot_y, 2.0, theme.knob_pointer);
631
632 if highlighted {
634 ctx.draw_line(pad_x, pad_y, pad_x + pad_w, pad_y, theme.accent, 1.0);
635 ctx.draw_line(
636 pad_x + pad_w,
637 pad_y,
638 pad_x + pad_w,
639 pad_y + pad_h,
640 theme.accent,
641 1.0,
642 );
643 ctx.draw_line(
644 pad_x + pad_w,
645 pad_y + pad_h,
646 pad_x,
647 pad_y + pad_h,
648 theme.accent,
649 1.0,
650 );
651 ctx.draw_line(pad_x, pad_y + pad_h, pad_x, pad_y, theme.accent, 1.0);
652 }
653
654 let label_size = 8.0;
656 let x_label_w = ctx.text_width(label_x, label_size);
657 let cx = x + width / 2.0;
658 ctx.draw_text(
659 label_x,
660 cx - x_label_w / 2.0,
661 y + height + 3.0,
662 label_size,
663 theme.text_dim,
664 );
665
666 if !label_y.is_empty() {
667 ctx.draw_text(
668 label_y,
669 pad_x + 2.0,
670 pad_y + 1.0,
671 label_size,
672 theme.text_dim,
673 );
674 }
675}
676
677pub fn draw_section_label(
679 ctx: &mut dyn RenderBackend,
680 x: f32,
681 y: f32,
682 w: f32,
683 label: &str,
684 theme: &Theme,
685) {
686 let size = 9.0;
687 let label_w = ctx.text_width(label, size);
688 ctx.draw_text(label, x + (w - label_w) / 2.0, y, size, theme.text_dim);
689}
690
691pub fn draw(
710 backend: &mut dyn RenderBackend,
711 layout: &Layout,
712 theme: &Theme,
713 snapshot: &ParamSnapshot<'_>,
714 state: &mut InteractionState,
715) {
716 match layout {
717 Layout::Rows(pl) => draw_rows(backend, pl, theme, snapshot, state),
718 Layout::Grid(gl) => draw_grid(backend, gl, theme, snapshot, state),
719 }
720 draw_dropdown_overlay(backend, theme, state);
721}
722
723fn resolve_wkind_to_type(
724 kind: Option<WidgetKind>,
725 param_id: u32,
726 snapshot: &ParamSnapshot<'_>,
727) -> WidgetType {
728 match kind {
729 Some(WidgetKind::Knob) => WidgetType::Knob,
730 Some(WidgetKind::Slider) => WidgetType::Slider,
731 Some(WidgetKind::Toggle) => WidgetType::Toggle,
732 Some(WidgetKind::Selector) => WidgetType::Selector,
733 Some(WidgetKind::Dropdown) => WidgetType::Dropdown,
734 Some(WidgetKind::Meter) => WidgetType::Meter,
735 Some(WidgetKind::XYPad) => WidgetType::XYPad,
736 None => (snapshot.widget_type)(param_id),
737 }
738}
739
740#[allow(clippy::cast_precision_loss)]
743fn draw_rows(
744 backend: &mut dyn RenderBackend,
745 pl: &PluginLayout,
746 theme: &Theme,
747 snapshot: &ParamSnapshot<'_>,
748 state: &mut InteractionState,
749) {
750 let w = pl.width;
751 let knob_size = pl.knob_size;
752 let pitch = knob_size + ROWS_COLUMN_GAP;
753 if !pl.titles.is_empty() {
754 draw_header(
755 backend,
756 0.0,
757 0.0,
758 w as f32,
759 HEADER_HEIGHT,
760 pl.titles.title,
761 pl.titles.subtitle,
762 theme,
763 );
764 }
765
766 let mut y = ROWS_LAYOUT_TOP;
767 let mut region_idx = 0usize;
768
769 for row in &pl.rows {
770 if let Some(label) = row.label {
771 draw_section_label(backend, 0.0, y, w as f32, label, theme);
772 y += ROWS_SECTION_LABEL_HEIGHT;
773 }
774
775 let total_cols: u32 = row.knobs.iter().map(|k| k.span.max(1)).sum();
776 let total_w = total_cols as f32 * pitch - ROWS_COLUMN_GAP;
777 let start_x = (w as f32 - total_w) / 2.0;
778
779 let mut col = 0u32;
780 for kd in &row.knobs {
781 let span = kd.span.max(1);
782 let x = start_x + col as f32 * pitch;
783 let widget_w = span as f32 * pitch - ROWS_COLUMN_GAP;
784 let widget_h = knob_size;
785
786 draw_widget_entry(
787 &mut WidgetDrawCtx {
788 backend,
789 theme,
790 snapshot,
791 state,
792 },
793 &WidgetDraw {
794 region_idx,
795 x,
796 y,
797 w: widget_w,
798 h: widget_h,
799 param_id: kd.param_id,
800 param_id_y: kd.param_id_y,
801 meter_ids: kd.meter_ids.as_deref(),
802 label: kd.label,
803 explicit_kind: kd.widget,
804 center_knob_in_cell: false, },
806 );
807
808 region_idx += 1;
809 col += span;
810 }
811
812 y += knob_size + ROWS_ROW_GAP;
813 }
814}
815
816#[allow(clippy::cast_precision_loss)]
819fn draw_grid(
820 backend: &mut dyn RenderBackend,
821 grid: &GridLayout,
822 theme: &Theme,
823 snapshot: &ParamSnapshot<'_>,
824 state: &mut InteractionState,
825) {
826 let w = grid.width;
827 if !grid.titles.is_empty() {
828 draw_header(
829 backend,
830 0.0,
831 0.0,
832 w as f32,
833 HEADER_HEIGHT,
834 grid.titles.title,
835 grid.titles.subtitle,
836 theme,
837 );
838 }
839
840 let header_h = grid.header_height();
841 let section_offsets = compute_section_offsets(grid);
842
843 for &(row_idx, label) in &grid.sections {
844 let y = header_h
845 + GRID_PADDING
846 + row_idx as f32 * (grid.cell_size + GRID_GAP)
847 + section_offsets[row_idx as usize]
848 - GRID_SECTION_H;
849 draw_section_label(backend, 0.0, y, w as f32, label, theme);
850 }
851
852 for (idx, gw) in grid.widgets.iter().enumerate() {
853 let x = GRID_PADDING + gw.col as f32 * (grid.cell_size + GRID_GAP);
854 let y = header_h
855 + GRID_PADDING
856 + gw.row as f32 * (grid.cell_size + GRID_GAP)
857 + section_offsets[gw.row as usize];
858 let widget_w = gw.col_span as f32 * (grid.cell_size + GRID_GAP) - GRID_GAP;
859 let widget_h = gw.row_span as f32 * (grid.cell_size + GRID_GAP) - GRID_GAP;
860
861 draw_widget_entry(
862 &mut WidgetDrawCtx {
863 backend,
864 theme,
865 snapshot,
866 state,
867 },
868 &WidgetDraw {
869 region_idx: idx,
870 x,
871 y,
872 w: widget_w,
873 h: widget_h,
874 param_id: gw.param_id,
875 param_id_y: gw.param_id_y,
876 meter_ids: gw.meter_ids.as_deref(),
877 label: gw.label,
878 explicit_kind: gw.widget,
879 center_knob_in_cell: true, },
881 );
882 }
883}
884
885struct WidgetDraw<'a> {
891 region_idx: usize,
892 x: f32,
894 y: f32,
895 w: f32,
896 h: f32,
897 param_id: u32,
898 param_id_y: Option<u32>,
902 meter_ids: Option<&'a [u32]>,
905 label: &'static str,
906 explicit_kind: Option<WidgetKind>,
907 center_knob_in_cell: bool,
910}
911
912struct WidgetDrawCtx<'a> {
916 backend: &'a mut dyn RenderBackend,
917 theme: &'a Theme,
918 snapshot: &'a ParamSnapshot<'a>,
919 state: &'a mut InteractionState,
920}
921
922fn draw_widget_entry(ctx: &mut WidgetDrawCtx<'_>, w: &WidgetDraw<'_>) {
923 let normalized = (ctx.snapshot.get_param)(w.param_id);
924 let value_text = (ctx.snapshot.format_param)(w.param_id);
925 let is_hovered = ctx.state.hover_idx == Some(w.region_idx);
926 let wtype = resolve_wkind_to_type(w.explicit_kind, w.param_id, ctx.snapshot);
927
928 match wtype {
929 WidgetType::Toggle => draw_toggle(
930 ctx.backend,
931 w.x,
932 w.y,
933 w.w,
934 w.h,
935 normalized,
936 w.label,
937 &value_text,
938 ctx.theme,
939 is_hovered,
940 ),
941 WidgetType::Slider => draw_slider(
942 ctx.backend,
943 w.x,
944 w.y,
945 w.w,
946 w.h,
947 normalized,
948 w.label,
949 &value_text,
950 ctx.theme,
951 is_hovered,
952 ),
953 WidgetType::Selector => draw_selector(
954 ctx.backend,
955 w.x,
956 w.y,
957 w.w,
958 w.h,
959 normalized,
960 w.label,
961 &value_text,
962 ctx.theme,
963 is_hovered,
964 ),
965 WidgetType::Dropdown => {
966 let is_open = ctx
967 .state
968 .dropdown
969 .as_ref()
970 .is_some_and(|dd| dd.region_idx == w.region_idx);
971 draw_dropdown(
972 ctx.backend,
973 w.x,
974 w.y,
975 w.w,
976 w.h,
977 normalized,
978 w.label,
979 &value_text,
980 ctx.theme,
981 is_hovered,
982 is_open,
983 );
984 let anchor_cy = w.y + w.h / 2.0 - 8.0;
989 if let Some(region) = ctx.state.knob_regions.get_mut(w.region_idx) {
990 region.dropdown_anchor_y = anchor_cy + DROPDOWN_BOX_HEIGHT / 2.0;
991 }
992 }
993 WidgetType::Meter => {
994 let fallback = [w.param_id];
995 let ids = w.meter_ids.unwrap_or(&fallback);
996 let levels: Vec<f32> = ids.iter().map(|&id| (ctx.snapshot.get_meter)(id)).collect();
997 draw_meter(ctx.backend, w.x, w.y, w.w, w.h, &levels, w.label, ctx.theme);
998 }
999 WidgetType::XYPad => {
1000 let val_y_id = w.param_id_y.unwrap_or(w.param_id);
1001 let vx = (ctx.snapshot.get_param)(w.param_id);
1002 let vy = (ctx.snapshot.get_param)(val_y_id);
1003 let x_name_str = (ctx.snapshot.param_name)(w.param_id);
1004 let y_name_str = (ctx.snapshot.param_name)(val_y_id);
1005 let x_name: &str = if x_name_str.is_empty() {
1006 w.label
1007 } else {
1008 &x_name_str
1009 };
1010 let y_name: &str = &y_name_str;
1011 draw_xy_pad(
1012 ctx.backend,
1013 w.x,
1014 w.y,
1015 w.w,
1016 w.h,
1017 vx,
1018 vy,
1019 x_name,
1020 y_name,
1021 ctx.theme,
1022 is_hovered,
1023 );
1024 }
1025 WidgetType::Knob => {
1026 if w.center_knob_in_cell {
1027 let knob_size = w.w.min(w.h);
1028 let kx = w.x + (w.w - knob_size) / 2.0;
1029 let ky = w.y + (w.h - knob_size) / 2.0;
1030 draw_knob(
1031 ctx.backend,
1032 kx,
1033 ky,
1034 knob_size,
1035 normalized,
1036 w.label,
1037 &value_text,
1038 ctx.theme,
1039 is_hovered,
1040 );
1041 } else {
1042 draw_knob(
1043 ctx.backend,
1044 w.x,
1045 w.y,
1046 w.h,
1047 normalized,
1048 w.label,
1049 &value_text,
1050 ctx.theme,
1051 is_hovered,
1052 );
1053 }
1054 }
1055 }
1056}
1057
1058fn draw_dropdown_overlay(backend: &mut dyn RenderBackend, theme: &Theme, state: &InteractionState) {
1059 if let Some(ref dd) = state.dropdown {
1060 let (px, py, pw, _) = dd.popup_rect;
1061 draw_dropdown_popup(
1062 backend,
1063 px,
1064 py,
1065 pw,
1066 &dd.options,
1067 dd.selected,
1068 dd.hover_option,
1069 dd.scroll_offset,
1070 dd.visible_count,
1071 theme,
1072 );
1073 }
1074}