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 Dropdown,
23 Meter,
24 XYPad,
25}
26
27pub fn draw_knob(
33 ctx: &mut dyn RenderBackend,
34 x: f32,
35 y: f32,
36 size: f32,
37 value: f32,
38 label: &str,
39 value_text: &str,
40 theme: &Theme,
41 highlighted: bool,
42) {
43 let cx = x + size / 2.0;
44 let cy = y + size / 2.0 - 5.0; let radius = size / 2.0 - 4.0;
46
47 let start_angle = 0.75 * PI; let end_angle = 2.25 * PI; let arc_start = start_angle;
52 let arc_end = end_angle;
53
54 ctx.stroke_arc(cx, cy, radius, arc_start, arc_end, theme.knob_track, 2.0);
56
57 let value_angle = arc_start + value * (arc_end - arc_start);
59 if value > 0.01 {
60 ctx.stroke_arc(cx, cy, radius, arc_start, value_angle, theme.knob_fill, 2.0);
61 }
62
63 let pointer_len = radius * 0.6;
65 let px = cx + pointer_len * value_angle.cos();
66 let py = cy + pointer_len * value_angle.sin();
67 ctx.draw_line(cx, cy, px, py, theme.knob_pointer, 1.5);
68
69 if highlighted {
71 ctx.stroke_arc(cx, cy, radius + 2.0, arc_start, arc_end, theme.accent, 1.0);
72 }
73
74 let val_size = 10.0;
76 let val_w = ctx.text_width(value_text, val_size);
77 ctx.draw_text(
78 value_text,
79 cx - val_w / 2.0,
80 y + size - 9.0,
81 val_size,
82 theme.text,
83 );
84
85 let label_size = 9.0;
87 let label_w = ctx.text_width(label, label_size);
88 ctx.draw_text(
89 label,
90 cx - label_w / 2.0,
91 y + size + 2.0,
92 label_size,
93 theme.text_dim,
94 );
95}
96
97pub fn draw_header(
101 ctx: &mut dyn RenderBackend,
102 x: f32,
103 y: f32,
104 w: f32,
105 h: f32,
106 title: Option<&str>,
107 subtitle: Option<&str>,
108 theme: &Theme,
109) {
110 ctx.fill_rect(x, y, w, h, theme.header_bg);
111
112 if let Some(title) = title {
113 let title_size = 12.0;
114 ctx.draw_text(
115 title,
116 x + 10.0,
117 y + (h - title_size) / 2.0 - 1.0,
118 title_size,
119 theme.header_text,
120 );
121 }
122
123 if let Some(subtitle) = subtitle {
124 let sub_size = 9.0;
125 let sub_w = ctx.text_width(subtitle, sub_size);
126 ctx.draw_text(
127 subtitle,
128 x + w - sub_w - 10.0,
129 y + (h - sub_size) / 2.0 - 1.0,
130 sub_size,
131 theme.text_dim,
132 );
133 }
134}
135
136pub fn draw_slider(
140 ctx: &mut dyn RenderBackend,
141 x: f32,
142 y: f32,
143 width: f32,
144 height: f32,
145 value: f32,
146 label: &str,
147 value_text: &str,
148 theme: &Theme,
149 highlighted: bool,
150) {
151 let track_y = y + height / 2.0 - 5.0;
152 let track_h = 3.0;
153 let margin = 4.0;
154 let track_w = width - margin * 2.0;
155
156 ctx.fill_rect(x + margin, track_y, track_w, track_h, theme.knob_track);
158
159 let fill_w = track_w * value;
161 if fill_w > 0.5 {
162 ctx.fill_rect(x + margin, track_y, fill_w, track_h, theme.knob_fill);
163 }
164
165 let thumb_x = x + margin + fill_w;
167 let thumb_r = 4.0;
168 ctx.fill_circle(
169 thumb_x,
170 track_y + track_h / 2.0,
171 thumb_r,
172 theme.knob_pointer,
173 );
174 if highlighted {
175 ctx.fill_circle(
176 thumb_x,
177 track_y + track_h / 2.0,
178 thumb_r + 1.5,
179 theme.accent,
180 );
181 ctx.fill_circle(
182 thumb_x,
183 track_y + track_h / 2.0,
184 thumb_r,
185 theme.knob_pointer,
186 );
187 }
188
189 let val_size = 10.0;
191 let cx = x + width / 2.0;
192 let val_w = ctx.text_width(value_text, val_size);
193 ctx.draw_text(
194 value_text,
195 cx - val_w / 2.0,
196 y + height - 9.0,
197 val_size,
198 theme.text,
199 );
200
201 let label_size = 9.0;
203 let label_w = ctx.text_width(label, label_size);
204 ctx.draw_text(
205 label,
206 cx - label_w / 2.0,
207 y + height + 2.0,
208 label_size,
209 theme.text_dim,
210 );
211}
212
213pub fn draw_toggle(
217 ctx: &mut dyn RenderBackend,
218 x: f32,
219 y: f32,
220 width: f32,
221 height: f32,
222 value: f32,
223 label: &str,
224 value_text: &str,
225 theme: &Theme,
226 highlighted: bool,
227) {
228 let is_on = value > 0.5;
229 let cx = x + width / 2.0;
230 let cy = y + height / 2.0 - 5.0;
231
232 let track_w = 20.0;
234 let track_h = 10.0;
235 let track_x = cx - track_w / 2.0;
236 let track_y = cy - track_h / 2.0;
237 let bg = if is_on {
238 theme.knob_fill
239 } else {
240 theme.knob_track
241 };
242 ctx.fill_rect(track_x, track_y, track_w, track_h, bg);
243
244 let thumb_x = if is_on {
246 track_x + track_w - track_h / 2.0
247 } else {
248 track_x + track_h / 2.0
249 };
250 ctx.fill_circle(thumb_x, cy, track_h / 2.0 - 1.0, theme.knob_pointer);
251
252 if highlighted {
253 ctx.fill_rect(
254 track_x - 1.0,
255 track_y - 1.0,
256 track_w + 2.0,
257 track_h + 2.0,
258 theme.accent,
259 );
260 ctx.fill_rect(track_x, track_y, track_w, track_h, bg);
261 ctx.fill_circle(thumb_x, cy, track_h / 2.0 - 1.0, theme.knob_pointer);
262 }
263
264 let val_size = 10.0;
266 let val_w = ctx.text_width(value_text, val_size);
267 ctx.draw_text(
268 value_text,
269 cx - val_w / 2.0,
270 y + height - 9.0,
271 val_size,
272 theme.text,
273 );
274
275 let label_size = 9.0;
277 let label_w = ctx.text_width(label, label_size);
278 ctx.draw_text(
279 label,
280 cx - label_w / 2.0,
281 y + height + 2.0,
282 label_size,
283 theme.text_dim,
284 );
285}
286
287pub fn draw_dropdown(
291 ctx: &mut dyn RenderBackend,
292 x: f32,
293 y: f32,
294 width: f32,
295 height: f32,
296 _value: f32,
297 label: &str,
298 value_text: &str,
299 theme: &Theme,
300 highlighted: bool,
301 is_open: bool,
302) {
303 let cx = x + width / 2.0;
304 let cy = y + height / 2.0 - 8.0;
305
306 let val_size = 10.0;
307 let arrow_pad = 14.0;
308 let val_w = ctx.text_width(value_text, val_size);
309 let box_w = (val_w + arrow_pad + 12.0).max(width - 12.0);
310 let box_h = DROPDOWN_BOX_HEIGHT;
311 let box_x = cx - box_w / 2.0;
312 let box_y = cy - box_h / 2.0;
313 let bg = if is_open || highlighted {
314 theme.accent
315 } else {
316 theme.knob_track
317 };
318 ctx.fill_rect(box_x, box_y, box_w, box_h, bg);
319
320 ctx.draw_text(
322 value_text,
323 box_x + 6.0,
324 cy - val_size / 2.0,
325 val_size,
326 theme.text,
327 );
328
329 let arrow_size = 8.0;
331 let arrow = if is_open { "\u{25B2}" } else { "\u{25BC}" }; let aw = ctx.text_width(arrow, arrow_size);
333 ctx.draw_text(
334 arrow,
335 box_x + box_w - aw - 4.0,
336 cy - arrow_size / 2.0,
337 arrow_size,
338 theme.text_dim,
339 );
340
341 let label_size = 9.0;
343 let label_w = ctx.text_width(label, label_size);
344 ctx.draw_text(
345 label,
346 cx - label_w / 2.0,
347 y + height + 2.0,
348 label_size,
349 theme.text_dim,
350 );
351}
352
353#[allow(clippy::cast_precision_loss)]
360pub fn draw_dropdown_popup(
361 ctx: &mut dyn RenderBackend,
362 x: f32,
363 y: f32,
364 width: f32,
365 options: &[String],
366 selected_index: usize,
367 hover_index: Option<usize>,
368 scroll_offset: usize,
369 visible_count: usize,
370 theme: &Theme,
371) {
372 let item_h = 18.0;
373 let padding = 4.0;
374 let popup_w = width.max(80.0);
375 let popup_h = visible_count as f32 * item_h + padding * 2.0;
376 let popup_x = x;
377 let popup_y = y;
378
379 ctx.fill_rect(popup_x, popup_y, popup_w, popup_h, theme.surface);
381 ctx.draw_line(
383 popup_x,
384 popup_y,
385 popup_x + popup_w,
386 popup_y,
387 theme.text_dim,
388 1.0,
389 );
390 ctx.draw_line(
391 popup_x + popup_w,
392 popup_y,
393 popup_x + popup_w,
394 popup_y + popup_h,
395 theme.text_dim,
396 1.0,
397 );
398 ctx.draw_line(
399 popup_x + popup_w,
400 popup_y + popup_h,
401 popup_x,
402 popup_y + popup_h,
403 theme.text_dim,
404 1.0,
405 );
406 ctx.draw_line(
407 popup_x,
408 popup_y + popup_h,
409 popup_x,
410 popup_y,
411 theme.text_dim,
412 1.0,
413 );
414
415 let text_size = 10.0;
416 let visible_end = (scroll_offset + visible_count).min(options.len());
417 for (vis_i, abs_i) in (scroll_offset..visible_end).enumerate() {
418 let iy = popup_y + padding + vis_i as f32 * item_h;
419
420 if hover_index == Some(abs_i) {
422 ctx.fill_rect(popup_x + 1.0, iy, popup_w - 2.0, item_h, theme.accent);
423 } else if abs_i == selected_index {
424 ctx.fill_rect(popup_x + 1.0, iy, popup_w - 2.0, item_h, theme.knob_track);
425 }
426
427 ctx.draw_text(
428 &options[abs_i],
429 popup_x + 6.0,
430 iy + (item_h - text_size) / 2.0,
431 text_size,
432 theme.text,
433 );
434 }
435
436 let arrow_size = 8.0;
438 let cx = popup_x + popup_w / 2.0;
439 if scroll_offset > 0 {
440 let aw = ctx.text_width("\u{25B2}", arrow_size);
441 ctx.draw_text(
442 "\u{25B2}",
443 cx - aw / 2.0,
444 popup_y + 1.0,
445 arrow_size,
446 theme.text_dim,
447 );
448 }
449 if visible_end < options.len() {
450 let aw = ctx.text_width("\u{25BC}", arrow_size);
451 ctx.draw_text(
452 "\u{25BC}",
453 cx - aw / 2.0,
454 popup_y + popup_h - arrow_size - 1.0,
455 arrow_size,
456 theme.text_dim,
457 );
458 }
459}
460
461#[allow(clippy::cast_precision_loss)]
467pub fn draw_meter(
468 ctx: &mut dyn RenderBackend,
469 x: f32,
470 y: f32,
471 width: f32,
472 height: f32,
473 levels: &[f32],
474 label: &str,
475 theme: &Theme,
476) {
477 let cx = x + width / 2.0;
478 let num = levels.len().max(1);
479 let bar_w = 4.0f32;
480 let gap = 2.0f32;
481 let total_bar_w = num as f32 * bar_w + (num as f32 - 1.0).max(0.0) * gap;
482 let bar_h = height - 4.0; let bar_start_x = cx - total_bar_w / 2.0;
484 let bar_y = y + 2.0;
485
486 for (i, &level) in levels.iter().enumerate() {
487 let bx = bar_start_x + i as f32 * (bar_w + gap);
488
489 ctx.fill_rect(bx, bar_y, bar_w, bar_h, theme.knob_track);
491
492 let display = truce_core::meter_display(level);
494 let fill_h = bar_h * display;
495 if fill_h > 0.5 {
496 let color = if display > 0.95 {
498 Color::rgb(0.88, 0.27, 0.27)
499 } else {
500 theme.knob_fill
501 };
502 ctx.fill_rect(bx, bar_y + bar_h - fill_h, bar_w, fill_h, color);
503 }
504 }
505
506 let label_size = 8.0;
508 let label_w = ctx.text_width(label, label_size);
509 ctx.draw_text(
510 label,
511 cx - label_w / 2.0,
512 y + height + 4.0,
513 label_size,
514 theme.text_dim,
515 );
516}
517
518pub fn draw_xy_pad(
522 ctx: &mut dyn RenderBackend,
523 x: f32,
524 y: f32,
525 width: f32,
526 height: f32,
527 value_x: f32,
528 value_y: f32,
529 label_x: &str,
530 label_y: &str,
531 theme: &Theme,
532 highlighted: bool,
533) {
534 let pad_margin = 4.0;
535 let pad_x = x + pad_margin;
536 let pad_y = y + pad_margin;
537 let pad_w = width - pad_margin * 2.0;
538 let pad_h = height - pad_margin * 2.0;
539
540 ctx.fill_rect(pad_x, pad_y, pad_w, pad_h, theme.knob_track);
542
543 let dot_x = pad_x + value_x.clamp(0.0, 1.0) * pad_w;
545 let dot_y = pad_y + (1.0 - value_y.clamp(0.0, 1.0)) * pad_h; let line_color = theme.text_dim;
547 ctx.draw_line(dot_x, pad_y, dot_x, pad_y + pad_h, line_color, 1.0);
548 ctx.draw_line(pad_x, dot_y, pad_x + pad_w, dot_y, line_color, 1.0);
549
550 let dot_color = if highlighted {
552 theme.accent
553 } else {
554 theme.knob_fill
555 };
556 ctx.fill_circle(dot_x, dot_y, 3.0, dot_color);
557 ctx.fill_circle(dot_x, dot_y, 2.0, theme.knob_pointer);
558
559 if highlighted {
561 ctx.draw_line(pad_x, pad_y, pad_x + pad_w, pad_y, theme.accent, 1.0);
562 ctx.draw_line(
563 pad_x + pad_w,
564 pad_y,
565 pad_x + pad_w,
566 pad_y + pad_h,
567 theme.accent,
568 1.0,
569 );
570 ctx.draw_line(
571 pad_x + pad_w,
572 pad_y + pad_h,
573 pad_x,
574 pad_y + pad_h,
575 theme.accent,
576 1.0,
577 );
578 ctx.draw_line(pad_x, pad_y + pad_h, pad_x, pad_y, theme.accent, 1.0);
579 }
580
581 let label_size = 8.0;
583 let x_label_w = ctx.text_width(label_x, label_size);
584 let cx = x + width / 2.0;
585 ctx.draw_text(
586 label_x,
587 cx - x_label_w / 2.0,
588 y + height + 3.0,
589 label_size,
590 theme.text_dim,
591 );
592
593 if !label_y.is_empty() {
594 ctx.draw_text(
595 label_y,
596 pad_x + 2.0,
597 pad_y + 1.0,
598 label_size,
599 theme.text_dim,
600 );
601 }
602}
603
604pub fn draw_section_label(
606 ctx: &mut dyn RenderBackend,
607 x: f32,
608 y: f32,
609 w: f32,
610 label: &str,
611 theme: &Theme,
612) {
613 let size = 9.0;
614 let label_w = ctx.text_width(label, size);
615 ctx.draw_text(label, x + (w - label_w) / 2.0, y, size, theme.text_dim);
616}
617
618pub fn draw(
637 backend: &mut dyn RenderBackend,
638 layout: &Layout,
639 theme: &Theme,
640 snapshot: &ParamSnapshot<'_>,
641 state: &mut InteractionState,
642) {
643 match layout {
644 Layout::Rows(pl) => draw_rows(backend, pl, theme, snapshot, state),
645 Layout::Grid(gl) => draw_grid(backend, gl, theme, snapshot, state),
646 }
647 draw_dropdown_overlay(backend, theme, state);
648}
649
650fn resolve_wkind_to_type(
651 kind: Option<WidgetKind>,
652 param_id: u32,
653 snapshot: &ParamSnapshot<'_>,
654) -> WidgetType {
655 match kind {
656 Some(WidgetKind::Knob) => WidgetType::Knob,
657 Some(WidgetKind::Slider) => WidgetType::Slider,
658 Some(WidgetKind::Toggle) => WidgetType::Toggle,
659 Some(WidgetKind::Dropdown) => WidgetType::Dropdown,
660 Some(WidgetKind::Meter) => WidgetType::Meter,
661 Some(WidgetKind::XYPad) => WidgetType::XYPad,
662 None => (snapshot.widget_type)(param_id),
663 }
664}
665
666#[allow(clippy::cast_precision_loss)]
669fn draw_rows(
670 backend: &mut dyn RenderBackend,
671 pl: &PluginLayout,
672 theme: &Theme,
673 snapshot: &ParamSnapshot<'_>,
674 state: &mut InteractionState,
675) {
676 let w = pl.width;
677 let knob_size = pl.knob_size;
678 let pitch = knob_size + ROWS_COLUMN_GAP;
679 if !pl.titles.is_empty() {
680 draw_header(
681 backend,
682 0.0,
683 0.0,
684 w as f32,
685 HEADER_HEIGHT,
686 pl.titles.title,
687 pl.titles.subtitle,
688 theme,
689 );
690 }
691
692 let mut y = ROWS_LAYOUT_TOP;
693 let mut region_idx = 0usize;
694
695 for row in &pl.rows {
696 if let Some(label) = row.label {
697 draw_section_label(backend, 0.0, y, w as f32, label, theme);
698 y += ROWS_SECTION_LABEL_HEIGHT;
699 }
700
701 let total_cols: u32 = row.knobs.iter().map(|k| k.span.max(1)).sum();
702 let total_w = total_cols as f32 * pitch - ROWS_COLUMN_GAP;
703 let start_x = (w as f32 - total_w) / 2.0;
704
705 let mut col = 0u32;
706 for kd in &row.knobs {
707 let span = kd.span.max(1);
708 let x = start_x + col as f32 * pitch;
709 let widget_w = span as f32 * pitch - ROWS_COLUMN_GAP;
710 let widget_h = knob_size;
711
712 draw_widget_entry(
713 &mut WidgetDrawCtx {
714 backend,
715 theme,
716 snapshot,
717 state,
718 },
719 &WidgetDraw {
720 region_idx,
721 x,
722 y,
723 w: widget_w,
724 h: widget_h,
725 param_id: kd.param_id,
726 param_id_y: kd.param_id_y,
727 meter_ids: kd.meter_ids.as_deref(),
728 label: kd.label,
729 explicit_kind: kd.widget,
730 center_knob_in_cell: false, },
732 );
733
734 region_idx += 1;
735 col += span;
736 }
737
738 y += knob_size + ROWS_ROW_GAP;
739 }
740}
741
742#[allow(clippy::cast_precision_loss)]
745fn draw_grid(
746 backend: &mut dyn RenderBackend,
747 grid: &GridLayout,
748 theme: &Theme,
749 snapshot: &ParamSnapshot<'_>,
750 state: &mut InteractionState,
751) {
752 let w = grid.width;
753 if !grid.titles.is_empty() {
754 draw_header(
755 backend,
756 0.0,
757 0.0,
758 w as f32,
759 HEADER_HEIGHT,
760 grid.titles.title,
761 grid.titles.subtitle,
762 theme,
763 );
764 }
765
766 let header_h = grid.header_height();
767 let section_offsets = compute_section_offsets(grid);
768
769 for &(row_idx, label) in &grid.sections {
770 let y = header_h
771 + GRID_PADDING
772 + row_idx as f32 * (grid.cell_size + GRID_GAP)
773 + section_offsets[row_idx as usize]
774 - GRID_SECTION_H;
775 draw_section_label(backend, 0.0, y, w as f32, label, theme);
776 }
777
778 for (idx, gw) in grid.widgets.iter().enumerate() {
779 let x = GRID_PADDING + gw.col as f32 * (grid.cell_size + GRID_GAP);
780 let y = header_h
781 + GRID_PADDING
782 + gw.row as f32 * (grid.cell_size + GRID_GAP)
783 + section_offsets[gw.row as usize];
784 let widget_w = gw.col_span as f32 * (grid.cell_size + GRID_GAP) - GRID_GAP;
785 let widget_h = gw.row_span as f32 * (grid.cell_size + GRID_GAP) - GRID_GAP;
786
787 draw_widget_entry(
788 &mut WidgetDrawCtx {
789 backend,
790 theme,
791 snapshot,
792 state,
793 },
794 &WidgetDraw {
795 region_idx: idx,
796 x,
797 y,
798 w: widget_w,
799 h: widget_h,
800 param_id: gw.param_id,
801 param_id_y: gw.param_id_y,
802 meter_ids: gw.meter_ids.as_deref(),
803 label: gw.label,
804 explicit_kind: gw.widget,
805 center_knob_in_cell: true, },
807 );
808 }
809}
810
811struct WidgetDraw<'a> {
817 region_idx: usize,
818 x: f32,
820 y: f32,
821 w: f32,
822 h: f32,
823 param_id: u32,
824 param_id_y: Option<u32>,
828 meter_ids: Option<&'a [u32]>,
831 label: &'static str,
832 explicit_kind: Option<WidgetKind>,
833 center_knob_in_cell: bool,
836}
837
838struct WidgetDrawCtx<'a> {
842 backend: &'a mut dyn RenderBackend,
843 theme: &'a Theme,
844 snapshot: &'a ParamSnapshot<'a>,
845 state: &'a mut InteractionState,
846}
847
848fn draw_widget_entry(ctx: &mut WidgetDrawCtx<'_>, w: &WidgetDraw<'_>) {
849 let normalized = (ctx.snapshot.get_param)(w.param_id);
850 let value_text = (ctx.snapshot.format_param)(w.param_id);
851 let is_hovered = ctx.state.hover_idx == Some(w.region_idx);
852 let wtype = resolve_wkind_to_type(w.explicit_kind, w.param_id, ctx.snapshot);
853
854 match wtype {
855 WidgetType::Toggle => draw_toggle(
856 ctx.backend,
857 w.x,
858 w.y,
859 w.w,
860 w.h,
861 normalized,
862 w.label,
863 &value_text,
864 ctx.theme,
865 is_hovered,
866 ),
867 WidgetType::Slider => draw_slider(
868 ctx.backend,
869 w.x,
870 w.y,
871 w.w,
872 w.h,
873 normalized,
874 w.label,
875 &value_text,
876 ctx.theme,
877 is_hovered,
878 ),
879 WidgetType::Dropdown => {
880 let is_open = ctx
881 .state
882 .dropdown
883 .as_ref()
884 .is_some_and(|dd| dd.region_idx == w.region_idx);
885 draw_dropdown(
886 ctx.backend,
887 w.x,
888 w.y,
889 w.w,
890 w.h,
891 normalized,
892 w.label,
893 &value_text,
894 ctx.theme,
895 is_hovered,
896 is_open,
897 );
898 let anchor_cy = w.y + w.h / 2.0 - 8.0;
903 if let Some(region) = ctx.state.knob_regions.get_mut(w.region_idx) {
904 region.dropdown_anchor_y = anchor_cy + DROPDOWN_BOX_HEIGHT / 2.0;
905 }
906 }
907 WidgetType::Meter => {
908 let fallback = [w.param_id];
909 let ids = w.meter_ids.unwrap_or(&fallback);
910 let levels: Vec<f32> = ids.iter().map(|&id| (ctx.snapshot.get_meter)(id)).collect();
911 draw_meter(ctx.backend, w.x, w.y, w.w, w.h, &levels, w.label, ctx.theme);
912 }
913 WidgetType::XYPad => {
914 let val_y_id = w.param_id_y.unwrap_or(w.param_id);
915 let vx = (ctx.snapshot.get_param)(w.param_id);
916 let vy = (ctx.snapshot.get_param)(val_y_id);
917 let x_name_str = (ctx.snapshot.param_name)(w.param_id);
918 let y_name_str = (ctx.snapshot.param_name)(val_y_id);
919 let x_name: &str = if x_name_str.is_empty() {
920 w.label
921 } else {
922 &x_name_str
923 };
924 let y_name: &str = &y_name_str;
925 draw_xy_pad(
926 ctx.backend,
927 w.x,
928 w.y,
929 w.w,
930 w.h,
931 vx,
932 vy,
933 x_name,
934 y_name,
935 ctx.theme,
936 is_hovered,
937 );
938 }
939 WidgetType::Knob => {
940 if w.center_knob_in_cell {
941 let knob_size = w.w.min(w.h);
942 let kx = w.x + (w.w - knob_size) / 2.0;
943 let ky = w.y + (w.h - knob_size) / 2.0;
944 draw_knob(
945 ctx.backend,
946 kx,
947 ky,
948 knob_size,
949 normalized,
950 w.label,
951 &value_text,
952 ctx.theme,
953 is_hovered,
954 );
955 } else {
956 draw_knob(
957 ctx.backend,
958 w.x,
959 w.y,
960 w.h,
961 normalized,
962 w.label,
963 &value_text,
964 ctx.theme,
965 is_hovered,
966 );
967 }
968 }
969 }
970}
971
972fn draw_dropdown_overlay(backend: &mut dyn RenderBackend, theme: &Theme, state: &InteractionState) {
973 if let Some(ref dd) = state.dropdown {
974 let (px, py, pw, _) = dd.popup_rect;
975 draw_dropdown_popup(
976 backend,
977 px,
978 py,
979 pw,
980 &dd.options,
981 dd.selected,
982 dd.hover_option,
983 dd.scroll_offset,
984 dd.visible_count,
985 theme,
986 );
987 }
988}