1#[cfg(feature = "cpu")]
9use std::ptr;
10use std::sync::Arc;
11#[cfg(feature = "cpu")]
12use std::sync::Mutex;
13use std::sync::atomic::{AtomicBool, Ordering};
14
15use truce_core::Float;
16#[cfg(feature = "cpu")]
17use truce_core::editor::Editor;
18#[cfg(feature = "cpu")]
19use truce_core::editor::RawWindowHandle;
20use truce_core::editor::{PluginContext, PluginContextReadF32};
21use truce_params::Params;
22
23#[cfg(feature = "cpu")]
24use crate::backend_cpu::CpuBackend;
25use crate::interaction::{self, InputEvent, InteractionState, ParamEdit};
26use crate::layout::{GridLayout, Layout, PluginLayout};
27#[cfg(feature = "cpu")]
28use crate::platform::EditorScale;
29use crate::render::RenderBackend;
30use crate::render_core::{
31 EditorSnapshotClosures, build_snapshot_closures as build_snapshot_closures_impl,
32 render_widgets as render_widgets_impl,
33};
34use crate::theme::Theme;
35use crate::widgets;
36
37pub struct BuiltinEditor<P: Params> {
42 params: Arc<P>,
43 layout: Layout,
44 theme: Theme,
45 #[cfg(feature = "cpu")]
50 backend: Option<CpuBackend>,
51 interaction: InteractionState,
52 context: Option<PluginContext>,
53 #[cfg(feature = "cpu")]
56 window: Option<baseview::WindowHandle>,
57 #[cfg(feature = "cpu")]
63 blit_backend: Option<SharedBackend>,
64 needs_repaint: Arc<AtomicBool>,
72 #[cfg(feature = "cpu")]
79 last_painted_values: Vec<f32>,
80 #[cfg(feature = "cpu")]
88 scale: EditorScale,
89}
90
91unsafe impl<P: Params> Send for BuiltinEditor<P> {}
101
102impl<P: Params + 'static> BuiltinEditor<P> {
103 pub fn request_repaint(&self) {
108 self.needs_repaint.store(true, Ordering::Release);
109 }
110
111 #[cfg(feature = "cpu")]
113 fn take_needs_repaint(&self) -> bool {
114 self.needs_repaint.swap(false, Ordering::AcqRel)
115 }
116
117 #[cfg(feature = "cpu")]
126 fn detect_host_param_changes(&mut self) {
127 let regions = &self.interaction.knob_regions;
128 if regions.len() != self.last_painted_values.len() {
129 self.request_repaint();
132 return;
133 }
134 for (i, region) in regions.iter().enumerate() {
135 if (region.normalized_value - self.last_painted_values[i]).abs() > f32::EPSILON {
136 self.request_repaint();
137 return;
138 }
139 }
140 }
141
142 #[cfg(feature = "cpu")]
146 fn stash_painted_values(&mut self) {
147 let regions = &self.interaction.knob_regions;
148 self.last_painted_values.resize(regions.len(), 0.0);
155 for (slot, region) in self.last_painted_values.iter_mut().zip(regions.iter()) {
156 *slot = region.normalized_value;
157 }
158 }
159
160 pub fn new(params: Arc<P>, layout: PluginLayout) -> Self {
161 Self::with_layout_inner(params, Layout::Rows(layout))
162 }
163
164 pub fn new_with_layout(params: Arc<P>, layout: Layout) -> Self {
165 Self::with_layout_inner(params, layout)
166 }
167
168 pub fn new_grid(params: Arc<P>, layout: GridLayout) -> Self {
169 Self::with_layout_inner(params, Layout::Grid(layout))
170 }
171
172 fn with_layout_inner(params: Arc<P>, layout: Layout) -> Self {
173 Self {
174 params,
175 layout,
176 theme: Theme::dark(),
177 #[cfg(feature = "cpu")]
178 backend: None,
179 interaction: InteractionState::default(),
180 context: None,
181 #[cfg(feature = "cpu")]
182 window: None,
183 #[cfg(feature = "cpu")]
184 blit_backend: None,
185 needs_repaint: Arc::new(AtomicBool::new(false)),
186 #[cfg(feature = "cpu")]
187 last_painted_values: Vec::new(),
188 #[cfg(feature = "cpu")]
189 scale: EditorScale::new(crate::backing_scale()),
190 }
191 }
192
193 #[must_use]
194 pub fn with_theme(mut self, theme: Theme) -> Self {
195 self.theme = theme;
196 self
197 }
198
199 #[cfg(feature = "cpu")]
211 pub fn render(&mut self) {
212 let (w, h) = (self.layout.width(), self.layout.height());
213 let scale = self.scale.get_f32();
214 let owned = self.build_snapshot_closures();
215 let snapshot = owned.as_snapshot();
216 let backend = self
217 .backend
218 .get_or_insert_with(|| CpuBackend::new(w, h, scale).expect("Failed to create backend"));
219 render_widgets_impl(
220 &self.layout,
221 &self.theme,
222 &mut self.interaction,
223 &snapshot,
224 backend,
225 );
226 }
227
228 fn build_snapshot_closures(&self) -> EditorSnapshotClosures {
235 build_snapshot_closures_impl(&self.params, self.context.as_ref())
236 }
237
238 fn apply_edit(&self, edit: ParamEdit) {
240 match edit {
241 ParamEdit::Begin { id } => {
242 if let Some(ref ctx) = self.context {
243 ctx.begin_edit(id);
244 }
245 }
246 ParamEdit::Set { id, normalized } => {
247 self.params.set_normalized(id, f64::from(normalized));
248 if let Some(ref ctx) = self.context {
249 ctx.set_param(id, f64::from(normalized));
250 }
251 self.request_repaint();
252 }
253 ParamEdit::End { id } => {
254 if let Some(ref ctx) = self.context {
255 ctx.end_edit(id);
256 }
257 }
258 }
259 }
260
261 pub fn dispatch_events(&mut self, events: &[InputEvent]) {
269 let hover_before = self.interaction.hover_idx;
270 let dd_before = self.interaction.dropdown_is_open();
271 let owned = self.build_snapshot_closures();
272 let snapshot = owned.as_snapshot();
273 let edits = interaction::dispatch(events, &self.layout, &snapshot, &mut self.interaction);
274 let had_edits = !edits.is_empty();
275 for e in edits {
276 self.apply_edit(e);
277 }
278 let explicit = self.interaction.take_repaint_request();
284 if had_edits
285 || explicit
286 || self.interaction.hover_idx != hover_before
287 || self.interaction.dropdown_is_open() != dd_before
288 {
289 self.request_repaint();
290 }
291 }
292
293 #[cfg(feature = "cpu")]
296 #[must_use]
297 pub fn pixel_data(&self) -> Option<&[u8]> {
298 self.backend
299 .as_ref()
300 .map(super::backend_cpu::CpuBackend::data)
301 }
302
303 #[must_use]
307 pub fn has_context(&self) -> bool {
308 self.context.is_some()
309 }
310
311 pub fn take_context(&mut self) -> Option<PluginContext> {
314 self.context.take()
315 }
316
317 pub fn set_context(&mut self, context: PluginContext) {
319 self.context = Some(context);
320 match &self.layout {
321 Layout::Rows(pl) => self.interaction.build_regions(pl),
322 Layout::Grid(gl) => self.interaction.build_regions_grid(gl),
323 }
324 }
325
326 #[must_use]
330 pub fn size(&self) -> (u32, u32) {
331 (self.layout.width(), self.layout.height())
332 }
333
334 pub fn state_changed(&mut self) {
338 self.request_repaint();
339 }
340
341 pub fn render_to(&mut self, backend: &mut dyn RenderBackend) {
346 update_interaction(self);
347 let owned = self.build_snapshot_closures();
348 let snapshot = owned.as_snapshot();
349 render_widgets_impl(
350 &self.layout,
351 &self.theme,
352 &mut self.interaction,
353 &snapshot,
354 backend,
355 );
356 }
357}
358
359#[cfg(test)]
363impl<P: Params + 'static> BuiltinEditor<P> {
364 fn on_mouse_down(&mut self, x: f32, y: f32) {
365 self.dispatch_events(&[InputEvent::MouseDown {
366 pointer_id: truce_gui_types::interaction::SINGLE_POINTER,
367 x,
368 y,
369 button: crate::interaction::MouseButton::Left,
370 }]);
371 }
372
373 fn on_mouse_up(&mut self, x: f32, y: f32) {
374 self.dispatch_events(&[InputEvent::MouseUp {
375 pointer_id: truce_gui_types::interaction::SINGLE_POINTER,
376 x,
377 y,
378 button: crate::interaction::MouseButton::Left,
379 }]);
380 }
381
382 fn on_mouse_moved(&mut self, x: f32, y: f32) {
383 self.dispatch_events(&[InputEvent::MouseMove {
384 pointer_id: truce_gui_types::interaction::SINGLE_POINTER,
385 x,
386 y,
387 }]);
388 }
389}
390
391pub fn update_interaction<P: Params + 'static>(editor: &mut BuiltinEditor<P>) {
401 match &editor.layout {
402 Layout::Rows(pl) => {
403 editor.interaction.build_regions(pl);
404 let mut flat_idx = 0usize;
405 for row in &pl.rows {
406 for knob_def in &row.knobs {
407 if let Some(region) = editor.interaction.knob_regions.get_mut(flat_idx) {
408 region.widget_type = resolve_widget_type(
409 knob_def.widget,
410 knob_def.param_id,
411 &*editor.params,
412 );
413 }
414 flat_idx += 1;
415 }
416 }
417 }
418 Layout::Grid(gl) => {
419 editor.interaction.build_regions_grid(gl);
420 for (idx, gw) in gl.widgets.iter().enumerate() {
421 if let Some(region) = editor.interaction.knob_regions.get_mut(idx) {
422 region.widget_type =
423 resolve_widget_type(gw.widget, gw.param_id, &*editor.params);
424 }
425 }
426 }
427 }
428 for region in &mut editor.interaction.knob_regions {
429 if let Some(ref ctx) = editor.context {
430 region.normalized_value = ctx.get_param(region.param_id);
432 } else {
433 region.normalized_value =
434 f32::from_f64(editor.params.get_normalized(region.param_id).unwrap_or(0.0));
435 }
436 }
437}
438
439#[cfg(feature = "cpu")]
454fn create_wgpu_backend(window: &mut baseview::Window, phys_w: u32, phys_h: u32) -> BlitBackend {
455 let mut desc = wgpu::InstanceDescriptor::new_without_display_handle();
456 desc.backends = wgpu::Backends::PRIMARY;
457 let instance = wgpu::Instance::new(desc);
458
459 let surface = unsafe { crate::platform::create_wgpu_surface(&instance, window) }
460 .expect("failed to create wgpu surface");
461
462 let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
463 power_preference: wgpu::PowerPreference::HighPerformance,
464 compatible_surface: Some(&surface),
465 force_fallback_adapter: false,
466 }))
467 .expect("no suitable GPU adapter");
468
469 let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
470 label: Some("truce-gui"),
471 required_features: wgpu::Features::empty(),
472 required_limits: wgpu::Limits::downlevel_defaults(),
473 experimental_features: wgpu::ExperimentalFeatures::default(),
474 memory_hints: wgpu::MemoryHints::Performance,
475 trace: wgpu::Trace::Off,
476 }))
477 .expect("failed to create wgpu device");
478
479 let caps = surface.get_capabilities(&adapter);
480 let format = caps
481 .formats
482 .iter()
483 .find(|f| f.is_srgb())
484 .copied()
485 .unwrap_or(caps.formats[0]);
486
487 let surface_config = wgpu::SurfaceConfiguration {
488 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
489 format,
490 width: phys_w,
491 height: phys_h,
492 present_mode: wgpu::PresentMode::AutoVsync,
493 desired_maximum_frame_latency: 2,
494 alpha_mode: wgpu::CompositeAlphaMode::Auto,
495 view_formats: vec![],
496 };
497 surface.configure(&device, &surface_config);
498
499 let blit = crate::blit::BlitPipeline::new(&device, format, phys_w, phys_h);
504
505 BlitBackend {
506 blit,
507 surface_config,
508 surface,
509 queue,
510 device,
511 }
512}
513
514#[cfg(feature = "cpu")]
523struct BlitBackend {
524 blit: crate::blit::BlitPipeline,
525 surface_config: wgpu::SurfaceConfiguration,
526 surface: wgpu::Surface<'static>,
527 queue: wgpu::Queue,
528 device: wgpu::Device,
529}
530
531#[cfg(feature = "cpu")]
532impl BlitBackend {
533 fn resize(&mut self, phys_w: u32, phys_h: u32) {
538 self.surface_config.width = phys_w.max(1);
539 self.surface_config.height = phys_h.max(1);
540 self.surface.configure(&self.device, &self.surface_config);
541 self.blit.resize(&self.device, phys_w, phys_h);
542 }
543}
544
545#[cfg(feature = "cpu")]
552type SharedBackend = Arc<Mutex<Option<BlitBackend>>>;
553
554#[cfg(feature = "cpu")]
555struct BuiltinWindowHandler<P: Params> {
556 editor: *mut BuiltinEditor<P>,
567 backend: SharedBackend,
568 translator: crate::interaction::BaseviewTranslator,
572 last_applied_scale: f32,
581}
582
583#[cfg(feature = "cpu")]
586unsafe impl<P: Params> Send for BuiltinWindowHandler<P> {}
587
588#[cfg(feature = "cpu")]
589impl<P: Params + 'static> baseview::WindowHandler for BuiltinWindowHandler<P> {
590 fn on_frame(&mut self, _window: &mut baseview::Window) {
591 let Ok(mut guard) = self.backend.lock() else {
598 return;
599 };
600 if guard.is_none() {
601 return;
604 }
605
606 let editor = unsafe { &mut *self.editor };
607
608 if let Some(cur_scale) = editor.scale.take_change(&mut self.last_applied_scale) {
616 let (lw, lh) = editor.size();
617 let phys_w = crate::platform::to_physical_px(lw, f64::from(cur_scale));
618 let phys_h = crate::platform::to_physical_px(lh, f64::from(cur_scale));
619 editor.backend = CpuBackend::new(lw, lh, cur_scale);
620 if let Some(backend) = guard.as_mut() {
621 backend.resize(phys_w, phys_h);
622 }
623 editor.request_repaint();
624 }
625
626 update_interaction(editor);
627 editor.detect_host_param_changes();
632 if !editor.take_needs_repaint() {
633 return;
634 }
635 editor.render();
636 editor.stash_painted_values();
637
638 if let Some(pixels) = editor.pixel_data() {
639 let backend = guard
640 .as_mut()
641 .expect("guard was checked Some above and the lock is still held");
642 let BlitBackend {
643 device,
644 queue,
645 surface,
646 blit,
647 ..
648 } = backend;
649 blit.update(queue, pixels);
650 let (wgpu::CurrentSurfaceTexture::Success(frame)
651 | wgpu::CurrentSurfaceTexture::Suboptimal(frame)) = surface.get_current_texture()
652 else {
653 return;
654 };
655 let view = frame
656 .texture
657 .create_view(&wgpu::TextureViewDescriptor::default());
658 let mut encoder =
659 device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
660 blit.render(&mut encoder, &view);
661 queue.submit(std::iter::once(encoder.finish()));
662 frame.present();
663 }
664 }
665
666 fn on_event(
667 &mut self,
668 window: &mut baseview::Window,
669 event: baseview::Event,
670 ) -> baseview::EventStatus {
671 #[cfg(not(target_os = "windows"))]
674 let _ = &window;
675
676 if let baseview::Event::Mouse(baseview::MouseEvent::ButtonPressed {
677 button: baseview::MouseButton::Left,
678 ..
679 }) = &event
680 {
681 #[cfg(target_os = "windows")]
687 {
688 if !window.has_focus() {
689 window.focus();
690 }
691 }
692 }
693
694 let Ok(guard) = self.backend.lock() else {
699 return baseview::EventStatus::Ignored;
700 };
701 if guard.is_none() {
702 return baseview::EventStatus::Ignored;
703 }
704
705 match event {
706 baseview::Event::Mouse(_) => {
707 let Some(input) = self.translator.translate(&event) else {
708 return baseview::EventStatus::Ignored;
709 };
710 let editor = unsafe { &mut *self.editor };
711 editor.dispatch_events(&[input]);
712 baseview::EventStatus::Captured
713 }
714 baseview::Event::Window(baseview::WindowEvent::Resized(info)) => {
715 let editor = unsafe { &mut *self.editor };
727 editor.scale.set(info.scale());
728 crate::platform::note_linux_scale_factor(info.scale());
729 baseview::EventStatus::Ignored
730 }
731 _ => baseview::EventStatus::Ignored,
732 }
733 }
734}
735
736fn resolve_widget_type<P: Params>(
742 widget: Option<crate::layout::WidgetKind>,
743 param_id: u32,
744 params: &P,
745) -> widgets::WidgetType {
746 match widget {
747 Some(crate::layout::WidgetKind::Knob) => widgets::WidgetType::Knob,
748 Some(crate::layout::WidgetKind::Slider) => widgets::WidgetType::Slider,
749 Some(crate::layout::WidgetKind::Toggle) => widgets::WidgetType::Toggle,
750 Some(crate::layout::WidgetKind::Selector) => widgets::WidgetType::Selector,
751 Some(crate::layout::WidgetKind::Dropdown) => widgets::WidgetType::Dropdown,
752 Some(crate::layout::WidgetKind::Meter) => widgets::WidgetType::Meter,
753 Some(crate::layout::WidgetKind::XYPad) => widgets::WidgetType::XYPad,
754 None => {
755 let param_info = params
756 .param_infos()
757 .iter()
758 .find(|i| i.id == param_id)
759 .copied();
760 match param_info.as_ref().map(|i| &i.range) {
761 Some(truce_params::ParamRange::Discrete { min: 0, max: 1 }) => {
762 widgets::WidgetType::Toggle
763 }
764 Some(truce_params::ParamRange::Enum { .. }) => widgets::WidgetType::Dropdown,
765 _ => widgets::WidgetType::Knob,
766 }
767 }
768 }
769}
770
771#[cfg(feature = "cpu")]
772impl<P: Params + 'static> Editor for BuiltinEditor<P> {
773 fn size(&self) -> (u32, u32) {
774 (self.layout.width(), self.layout.height())
775 }
776
777 fn state_changed(&mut self) {
778 self.request_repaint();
781 }
782
783 fn open(&mut self, parent: RawWindowHandle, context: PluginContext) {
784 let (w, h) = self.size();
785 self.scale
791 .set(crate::platform::query_backing_scale(&parent));
792 let scale = self.scale.get();
793 let scale_f32 = self.scale.get_f32();
794 self.backend = CpuBackend::new(w, h, scale_f32);
795 self.context = Some(context);
796
797 match &self.layout {
799 Layout::Rows(pl) => self.interaction.build_regions(pl),
800 Layout::Grid(gl) => self.interaction.build_regions_grid(gl),
801 }
802
803 self.render();
807 self.request_repaint();
808
809 let (lw, lh) = (f64::from(w), f64::from(h));
810 let phys_w = crate::platform::to_physical_px(w, scale);
811 let phys_h = crate::platform::to_physical_px(h, scale);
812
813 let options = baseview::WindowOpenOptions {
814 title: String::from("truce"),
815 size: baseview::Size::new(lw, lh),
816 scale: baseview::WindowScalePolicy::SystemScaleFactor,
817 };
818
819 let parent_wrapper = crate::platform::ParentWindow(parent);
820 let editor_addr = ptr::from_mut::<BuiltinEditor<P>>(self) as usize;
821
822 let shared_backend: SharedBackend = Arc::new(Mutex::new(None));
827 self.blit_backend = Some(shared_backend.clone());
828 let shared_for_handler = shared_backend;
829
830 let window = baseview::Window::open_parented(
831 &parent_wrapper,
832 options,
833 move |window: &mut baseview::Window| {
834 let mut backend = create_wgpu_backend(window, phys_w, phys_h);
835
836 let editor = unsafe { &mut *(editor_addr as *mut BuiltinEditor<P>) };
843 editor.render();
844 if let Some(pixels) = editor.pixel_data() {
845 let BlitBackend {
846 device,
847 queue,
848 surface,
849 blit,
850 ..
851 } = &mut backend;
852 blit.update(queue, pixels);
853 if let wgpu::CurrentSurfaceTexture::Success(frame)
854 | wgpu::CurrentSurfaceTexture::Suboptimal(frame) =
855 surface.get_current_texture()
856 {
857 let view = frame
858 .texture
859 .create_view(&wgpu::TextureViewDescriptor::default());
860 let mut encoder =
861 device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
862 label: None,
863 });
864 blit.render(&mut encoder, &view);
865 queue.submit(std::iter::once(encoder.finish()));
866 frame.present();
867 }
868 }
869
870 if let Ok(mut guard) = shared_for_handler.lock() {
877 *guard = Some(backend);
878 }
879
880 BuiltinWindowHandler {
881 editor: editor_addr as *mut BuiltinEditor<P>,
882 backend: shared_for_handler.clone(),
883 translator: crate::interaction::BaseviewTranslator::default(),
884 last_applied_scale: scale_f32,
885 }
886 },
887 );
888
889 self.window = Some(window);
890 }
891
892 fn set_scale_factor(&mut self, factor: f64) {
893 self.scale.set(factor);
898 }
899
900 fn close(&mut self) {
901 #[cfg(target_os = "macos")]
908 let pool = unsafe {
909 unsafe extern "C" {
910 fn objc_autoreleasePoolPush() -> *mut std::ffi::c_void;
911 }
912 objc_autoreleasePoolPush()
913 };
914
915 if let Some(shared) = self.blit_backend.take()
924 && let Ok(mut guard) = shared.lock()
925 && let Some(backend) = guard.take()
926 {
927 let BlitBackend {
928 blit,
929 surface,
930 surface_config,
931 queue,
932 device,
933 } = backend;
934 drop(surface_config);
935 drop(blit);
936 drop(surface);
937 drop(queue);
938 drop(device);
939 }
940
941 if let Some(mut window) = self.window.take() {
942 window.close();
943 }
944 self.context = None;
945 self.backend = None;
946
947 #[cfg(target_os = "macos")]
948 unsafe {
949 unsafe extern "C" {
950 fn objc_autoreleasePoolPop(pool: *mut std::ffi::c_void);
951 }
952 objc_autoreleasePoolPop(pool);
953 }
954 }
955
956 fn idle(&mut self) {
957 if self.window.is_none() {
961 self.render();
962 }
963 }
964
965 fn screenshot(
966 &mut self,
967 _params: Arc<dyn truce_params::Params>,
968 ) -> Option<(Vec<u8>, u32, u32)> {
969 let (lw, lh) = self.size();
976 let scale = self.scale.get_f32();
977 let mut backend = CpuBackend::new(lw, lh, scale)?;
978 self.render_to(&mut backend);
979 let pixels = backend.data().to_vec();
980 let (phys_w, phys_h) = (backend.width(), backend.height());
981 Some((pixels, phys_w, phys_h))
982 }
983}
984
985#[cfg(test)]
986mod tests {
987 #![allow(clippy::float_cmp, clippy::cast_precision_loss)]
990
991 use super::*;
992 use crate::layout::{GridLayout, GridWidget, Layout, section, widgets};
993 use crate::widgets::WidgetType;
994 use std::sync::Arc;
995 use std::sync::atomic::{AtomicU64, Ordering};
996 use truce_params::{ParamFlags, ParamInfo, ParamRange, ParamUnit, ParamValueKind, Params};
997
998 struct TestParams {
1001 values: [AtomicU64; 2],
1002 }
1003
1004 impl TestParams {
1005 fn new() -> Self {
1006 Self {
1007 values: [
1008 AtomicU64::new(0.0f64.to_bits()),
1009 AtomicU64::new(0.0f64.to_bits()),
1010 ],
1011 }
1012 }
1013 }
1014
1015 impl truce_params::__private::Sealed for TestParams {}
1016 impl Params for TestParams {
1017 fn param_infos(&self) -> Vec<ParamInfo> {
1018 vec![
1019 ParamInfo {
1020 id: 0,
1021 name: "Mode",
1022 short_name: "Mode",
1023 group: "",
1024 range: ParamRange::Enum { count: 4 },
1025 default_plain: 0.0,
1026 flags: ParamFlags::AUTOMATABLE,
1027 unit: ParamUnit::None,
1028 kind: ParamValueKind::Enum,
1029 },
1030 ParamInfo {
1031 id: 1,
1032 name: "Gain",
1033 short_name: "Gain",
1034 group: "",
1035 range: ParamRange::Linear { min: 0.0, max: 1.0 },
1036 default_plain: 0.5,
1037 flags: ParamFlags::AUTOMATABLE,
1038 unit: ParamUnit::None,
1039 kind: ParamValueKind::Float,
1040 },
1041 ]
1042 }
1043
1044 fn count(&self) -> usize {
1045 2
1046 }
1047
1048 fn get_normalized(&self, id: u32) -> Option<f64> {
1049 self.values
1050 .get(id as usize)
1051 .map(|v| f64::from_bits(v.load(Ordering::Relaxed)))
1052 }
1053
1054 fn set_normalized(&self, id: u32, value: f64) {
1055 if let Some(v) = self.values.get(id as usize) {
1056 v.store(value.to_bits(), Ordering::Relaxed);
1057 }
1058 }
1059
1060 fn get_plain(&self, id: u32) -> Option<f64> {
1061 let norm = self.get_normalized(id)?;
1062 let info = self.param_infos().iter().find(|i| i.id == id).copied()?;
1063 Some(info.range.denormalize(norm))
1064 }
1065
1066 fn set_plain(&self, id: u32, value: f64) {
1067 if let Some(info) = self.param_infos().iter().find(|i| i.id == id).copied() {
1068 self.set_normalized(id, info.range.normalize(value));
1069 }
1070 }
1071
1072 fn format_value(&self, _id: u32, value: f64) -> Option<String> {
1073 Some(format!("{value:.0}"))
1074 }
1075
1076 fn parse_value(&self, _id: u32, _text: &str) -> Option<f64> {
1077 None
1078 }
1079 fn snap_smoothers(&self) {}
1080 fn set_sample_rate(&self, _: f64) {}
1081
1082 fn collect_values(&self) -> (Vec<u32>, Vec<f64>) {
1083 let ids = vec![0, 1];
1084 let vals: Vec<f64> = ids
1085 .iter()
1086 .map(|&id| self.get_plain(id).unwrap_or(0.0))
1087 .collect();
1088 (ids, vals)
1089 }
1090
1091 fn restore_values(&self, values: &[(u32, f64)]) {
1092 for &(id, val) in values {
1093 self.set_plain(id, val);
1094 }
1095 }
1096 }
1097
1098 impl Default for TestParams {
1099 fn default() -> Self {
1100 Self::new()
1101 }
1102 }
1103
1104 fn make_editor() -> BuiltinEditor<TestParams> {
1108 let params = Arc::new(TestParams::new());
1109 let layout = GridLayout::build(vec![widgets(vec![
1110 GridWidget::dropdown(0u32, "Mode"),
1111 GridWidget::knob(1u32, "Gain"),
1112 ])]);
1113 let mut editor = BuiltinEditor::new_grid(params, layout);
1114 if let Layout::Grid(ref gl) = editor.layout {
1116 editor.interaction.build_regions_grid(gl);
1117 for (idx, gw) in gl.widgets.iter().enumerate() {
1118 if let Some(region) = editor.interaction.knob_regions.get_mut(idx) {
1119 region.widget_type =
1120 resolve_widget_type(gw.widget, gw.param_id, &*editor.params);
1121 }
1122 }
1123 }
1124 editor.render();
1126 editor
1127 }
1128
1129 fn make_editor_with_sections() -> BuiltinEditor<TestParams> {
1131 let params = Arc::new(TestParams::new());
1132 let layout = GridLayout::build(vec![
1133 section(
1134 "SECTION A",
1135 vec![
1136 GridWidget::knob(1u32, "Gain"),
1137 GridWidget::knob(1u32, "Gain 2"),
1138 ],
1139 ),
1140 section(
1141 "SECTION B",
1142 vec![
1143 GridWidget::dropdown(0u32, "Mode"),
1144 GridWidget::knob(1u32, "Gain 3"),
1145 ],
1146 ),
1147 ]);
1148 let mut editor = BuiltinEditor::new_grid(params, layout);
1149 if let Layout::Grid(ref gl) = editor.layout {
1150 editor.interaction.build_regions_grid(gl);
1151 for (idx, gw) in gl.widgets.iter().enumerate() {
1152 if let Some(region) = editor.interaction.knob_regions.get_mut(idx) {
1153 region.widget_type =
1154 resolve_widget_type(gw.widget, gw.param_id, &*editor.params);
1155 }
1156 }
1157 }
1158 editor.render();
1159 editor
1160 }
1161
1162 fn dropdown_center(editor: &BuiltinEditor<TestParams>) -> (f32, f32) {
1164 let region = editor
1165 .interaction
1166 .knob_regions
1167 .iter()
1168 .find(|r| r.widget_type == WidgetType::Dropdown)
1169 .expect("no dropdown in layout");
1170 (region.x + region.w / 2.0, region.y + region.h / 2.0)
1171 }
1172
1173 #[test]
1176 fn dropdown_click_opens() {
1177 let mut editor = make_editor();
1178 let (dx, dy) = dropdown_center(&editor);
1179
1180 editor.on_mouse_down(dx, dy);
1181 assert!(editor.interaction.dropdown_is_open());
1182 }
1183
1184 #[test]
1185 fn dropdown_click_toggles_closed() {
1186 let mut editor = make_editor();
1187 let (dx, dy) = dropdown_center(&editor);
1188
1189 editor.on_mouse_down(dx, dy);
1191 editor.on_mouse_up(dx, dy);
1192 assert!(editor.interaction.dropdown_is_open());
1193
1194 editor.on_mouse_down(dx, dy);
1196 assert!(!editor.interaction.dropdown_is_open());
1197 }
1198
1199 #[test]
1200 fn dropdown_click_outside_closes() {
1201 let mut editor = make_editor();
1202 let (dx, dy) = dropdown_center(&editor);
1203
1204 editor.on_mouse_down(dx, dy);
1205 editor.on_mouse_up(dx, dy);
1206 assert!(editor.interaction.dropdown_is_open());
1207
1208 editor.on_mouse_down(0.0, 0.0);
1210 assert!(!editor.interaction.dropdown_is_open());
1211 }
1212
1213 #[test]
1214 fn dropdown_click_option_selects_and_closes() {
1215 let mut editor = make_editor();
1216 let (dx, dy) = dropdown_center(&editor);
1217
1218 editor.on_mouse_down(dx, dy);
1219 editor.on_mouse_up(dx, dy);
1220 assert!(editor.interaction.dropdown_is_open());
1221
1222 let dd = editor.interaction.dropdown.as_ref().unwrap();
1224 let (px, py, _, _) = dd.popup_rect;
1225 let item_h = 18.0f32;
1226 let padding = 4.0f32;
1227 let option_y = py + padding + item_h + item_h / 2.0; editor.on_mouse_down(px + 10.0, option_y);
1233 editor.on_mouse_up(px + 10.0, option_y);
1234
1235 assert!(!editor.interaction.dropdown_is_open());
1236 let norm = editor.params.get_normalized(0).unwrap();
1238 let expected = 1.0 / 3.0;
1239 assert!(
1240 (norm - expected).abs() < 0.01,
1241 "expected {expected:.4}, got {norm}"
1242 );
1243 }
1244
1245 #[test]
1248 fn dropdown_anchor_set_after_render() {
1249 let editor = make_editor();
1250 let region = editor
1251 .interaction
1252 .knob_regions
1253 .iter()
1254 .find(|r| r.widget_type == WidgetType::Dropdown)
1255 .unwrap();
1256
1257 assert!(
1259 region.dropdown_anchor_y > region.y,
1260 "anchor {} should be below region.y {}",
1261 region.dropdown_anchor_y,
1262 region.y
1263 );
1264 assert!(
1265 region.dropdown_anchor_y < region.y + region.h,
1266 "anchor {} should be above region bottom {}",
1267 region.dropdown_anchor_y,
1268 region.y + region.h
1269 );
1270 }
1271
1272 #[test]
1273 fn dropdown_popup_uses_anchor() {
1274 let mut editor = make_editor();
1275 let (dx, dy) = dropdown_center(&editor);
1276
1277 editor.on_mouse_down(dx, dy);
1278 editor.on_mouse_up(dx, dy);
1279
1280 let dd = editor.interaction.dropdown.as_ref().unwrap();
1281 let region = &editor.interaction.knob_regions[dd.region_idx];
1282
1283 assert_eq!(dd.popup_rect.1, region.dropdown_anchor_y);
1287 }
1288
1289 #[test]
1290 fn dropdown_anchor_gap_stable_with_sections() {
1291 let editor_plain = make_editor();
1292 let editor_sections = make_editor_with_sections();
1293
1294 let r_plain = editor_plain
1295 .interaction
1296 .knob_regions
1297 .iter()
1298 .find(|r| r.widget_type == WidgetType::Dropdown)
1299 .unwrap();
1300 let r_sections = editor_sections
1301 .interaction
1302 .knob_regions
1303 .iter()
1304 .find(|r| r.widget_type == WidgetType::Dropdown)
1305 .unwrap();
1306
1307 let gap_plain = r_plain.dropdown_anchor_y - (r_plain.y + r_plain.h / 2.0);
1310 let gap_sections = r_sections.dropdown_anchor_y - (r_sections.y + r_sections.h / 2.0);
1311 assert!(
1312 (gap_plain - gap_sections).abs() < 0.1,
1313 "gap_plain={gap_plain}, gap_sections={gap_sections}"
1314 );
1315 }
1316
1317 struct ManyOptionParams {
1320 values: [AtomicU64; 2],
1321 }
1322
1323 impl ManyOptionParams {
1324 fn new() -> Self {
1325 Self {
1326 values: [
1327 AtomicU64::new(0.0f64.to_bits()),
1328 AtomicU64::new(0.0f64.to_bits()),
1329 ],
1330 }
1331 }
1332 }
1333
1334 impl truce_params::__private::Sealed for ManyOptionParams {}
1335 impl Params for ManyOptionParams {
1336 fn param_infos(&self) -> Vec<ParamInfo> {
1337 vec![
1338 ParamInfo {
1339 id: 0,
1340 name: "Note",
1341 short_name: "Note",
1342 group: "",
1343 range: ParamRange::Enum { count: 20 },
1344 default_plain: 0.0,
1345 flags: ParamFlags::AUTOMATABLE,
1346 unit: ParamUnit::None,
1347 kind: ParamValueKind::Enum,
1348 },
1349 ParamInfo {
1350 id: 1,
1351 name: "Gain",
1352 short_name: "Gain",
1353 group: "",
1354 range: ParamRange::Linear { min: 0.0, max: 1.0 },
1355 default_plain: 0.5,
1356 flags: ParamFlags::AUTOMATABLE,
1357 unit: ParamUnit::None,
1358 kind: ParamValueKind::Float,
1359 },
1360 ]
1361 }
1362
1363 fn count(&self) -> usize {
1364 2
1365 }
1366
1367 fn get_normalized(&self, id: u32) -> Option<f64> {
1368 self.values
1369 .get(id as usize)
1370 .map(|v| f64::from_bits(v.load(Ordering::Relaxed)))
1371 }
1372
1373 fn set_normalized(&self, id: u32, value: f64) {
1374 if let Some(v) = self.values.get(id as usize) {
1375 v.store(value.to_bits(), Ordering::Relaxed);
1376 }
1377 }
1378
1379 fn get_plain(&self, id: u32) -> Option<f64> {
1380 let norm = self.get_normalized(id)?;
1381 let info = self.param_infos().iter().find(|i| i.id == id).copied()?;
1382 Some(info.range.denormalize(norm))
1383 }
1384
1385 fn set_plain(&self, id: u32, value: f64) {
1386 if let Some(info) = self.param_infos().iter().find(|i| i.id == id).copied() {
1387 self.set_normalized(id, info.range.normalize(value));
1388 }
1389 }
1390
1391 fn format_value(&self, _id: u32, value: f64) -> Option<String> {
1392 Some(format!("{value:.0}"))
1393 }
1394
1395 fn parse_value(&self, _id: u32, _text: &str) -> Option<f64> {
1396 None
1397 }
1398 fn snap_smoothers(&self) {}
1399 fn set_sample_rate(&self, _: f64) {}
1400
1401 fn collect_values(&self) -> (Vec<u32>, Vec<f64>) {
1402 let ids = vec![0, 1];
1403 let vals: Vec<f64> = ids
1404 .iter()
1405 .map(|&id| self.get_plain(id).unwrap_or(0.0))
1406 .collect();
1407 (ids, vals)
1408 }
1409
1410 fn restore_values(&self, values: &[(u32, f64)]) {
1411 for &(id, val) in values {
1412 self.set_plain(id, val);
1413 }
1414 }
1415 }
1416
1417 impl Default for ManyOptionParams {
1418 fn default() -> Self {
1419 Self::new()
1420 }
1421 }
1422
1423 fn make_editor_bottom_dropdown() -> BuiltinEditor<TestParams> {
1427 let params = Arc::new(TestParams::new());
1428 let layout = GridLayout::build(vec![widgets(vec![
1430 GridWidget::knob(1u32, "K1"),
1431 GridWidget::knob(1u32, "K2"),
1432 GridWidget::knob(1u32, "K3"),
1433 GridWidget::knob(1u32, "K4"),
1434 GridWidget::dropdown(0u32, "Mode"),
1435 GridWidget::knob(1u32, "K5"),
1436 ])])
1437 .with_cols(2);
1438 let mut editor = BuiltinEditor::new_grid(params, layout);
1439 if let Layout::Grid(ref gl) = editor.layout {
1440 editor.interaction.build_regions_grid(gl);
1441 for (idx, gw) in gl.widgets.iter().enumerate() {
1442 if let Some(region) = editor.interaction.knob_regions.get_mut(idx) {
1443 region.widget_type =
1444 resolve_widget_type(gw.widget, gw.param_id, &*editor.params);
1445 }
1446 }
1447 }
1448 editor.render();
1449 editor
1450 }
1451
1452 fn make_editor_two_dropdowns() -> BuiltinEditor<TestParams> {
1454 let params = Arc::new(TestParams::new());
1455 let layout = GridLayout::build(vec![widgets(vec![
1456 GridWidget::dropdown(0u32, "Mode A"),
1457 GridWidget::dropdown(0u32, "Mode B"),
1458 ])]);
1459 let mut editor = BuiltinEditor::new_grid(params, layout);
1460 if let Layout::Grid(ref gl) = editor.layout {
1461 editor.interaction.build_regions_grid(gl);
1462 for (idx, gw) in gl.widgets.iter().enumerate() {
1463 if let Some(region) = editor.interaction.knob_regions.get_mut(idx) {
1464 region.widget_type =
1465 resolve_widget_type(gw.widget, gw.param_id, &*editor.params);
1466 }
1467 }
1468 }
1469 editor.render();
1470 editor
1471 }
1472
1473 fn make_editor_many_options() -> BuiltinEditor<ManyOptionParams> {
1475 let params = Arc::new(ManyOptionParams::new());
1476 let layout = GridLayout::build(vec![widgets(vec![
1477 GridWidget::dropdown(0u32, "Note"),
1478 GridWidget::knob(1u32, "Gain"),
1479 ])]);
1480 let mut editor = BuiltinEditor::new_grid(params, layout);
1481 if let Layout::Grid(ref gl) = editor.layout {
1482 editor.interaction.build_regions_grid(gl);
1483 for (idx, gw) in gl.widgets.iter().enumerate() {
1484 if let Some(region) = editor.interaction.knob_regions.get_mut(idx) {
1485 region.widget_type =
1486 resolve_widget_type(gw.widget, gw.param_id, &*editor.params);
1487 }
1488 }
1489 }
1490 editor.render();
1491 editor
1492 }
1493
1494 fn dropdown_center_many(editor: &BuiltinEditor<ManyOptionParams>) -> (f32, f32) {
1495 let region = editor
1496 .interaction
1497 .knob_regions
1498 .iter()
1499 .find(|r| r.widget_type == WidgetType::Dropdown)
1500 .expect("no dropdown in layout");
1501 (region.x + region.w / 2.0, region.y + region.h / 2.0)
1502 }
1503
1504 #[test]
1507 fn dropdown_anchors_below_button_scrolls_when_tight() {
1508 let mut editor = make_editor_bottom_dropdown();
1509 let (dx, dy) = {
1510 let region = editor
1511 .interaction
1512 .knob_regions
1513 .iter()
1514 .find(|r| r.widget_type == WidgetType::Dropdown)
1515 .unwrap();
1516 (region.x + region.w / 2.0, region.y + region.h / 2.0)
1517 };
1518
1519 editor.on_mouse_down(dx, dy);
1520 editor.on_mouse_up(dx, dy);
1521 assert!(editor.interaction.dropdown_is_open());
1522
1523 let dd = editor.interaction.dropdown.as_ref().unwrap();
1524 let region = &editor.interaction.knob_regions[dd.region_idx];
1525 let (_, popup_y, _, popup_h) = dd.popup_rect;
1526 let window_h = editor.layout.height() as f32;
1527
1528 assert_eq!(
1533 popup_y, region.dropdown_anchor_y,
1534 "popup must anchor at dropdown_anchor_y, got popup_y={popup_y}"
1535 );
1536 assert!(
1538 popup_y + popup_h <= window_h + 1.0,
1539 "popup bottom {} exceeds window height {window_h}",
1540 popup_y + popup_h
1541 );
1542 }
1543
1544 #[test]
1545 fn dropdown_clamps_horizontal_near_right_edge() {
1546 let mut editor = make_editor_two_dropdowns();
1547 let region = &editor.interaction.knob_regions[1];
1549 assert_eq!(region.widget_type, WidgetType::Dropdown);
1550 let dx = region.x + region.w / 2.0;
1551 let dy = region.y + region.h / 2.0;
1552
1553 editor.on_mouse_down(dx, dy);
1554 editor.on_mouse_up(dx, dy);
1555 assert!(editor.interaction.dropdown_is_open());
1556
1557 let dd = editor.interaction.dropdown.as_ref().unwrap();
1558 let (popup_x, _, popup_w, _) = dd.popup_rect;
1559 let window_w = editor.layout.width() as f32;
1560
1561 assert!(
1562 popup_x + popup_w <= window_w + 1.0,
1563 "popup right edge {} exceeds window width {window_w}",
1564 popup_x + popup_w
1565 );
1566 assert!(popup_x >= 0.0, "popup_x={popup_x} is negative");
1567 }
1568
1569 #[test]
1570 fn dropdown_scroll_long_list() {
1571 let mut editor = make_editor_many_options();
1572 let (dx, dy) = dropdown_center_many(&editor);
1573
1574 editor.on_mouse_down(dx, dy);
1575 editor.on_mouse_up(dx, dy);
1576 assert!(editor.interaction.dropdown_is_open());
1577
1578 let dd = editor.interaction.dropdown.as_ref().unwrap();
1579 assert!(
1581 dd.options.len() > dd.visible_count,
1582 "expected scroll: {} options, {} visible",
1583 dd.options.len(),
1584 dd.visible_count
1585 );
1586 assert_eq!(dd.scroll_offset, 0);
1587 }
1588
1589 #[test]
1590 fn dropdown_scroll_clamps_to_bounds() {
1591 let mut editor = make_editor_many_options();
1592 let (dx, dy) = dropdown_center_many(&editor);
1593
1594 editor.on_mouse_down(dx, dy);
1595 editor.on_mouse_up(dx, dy);
1596
1597 editor.interaction.dropdown_scroll(-10);
1599 assert_eq!(
1600 editor.interaction.dropdown.as_ref().unwrap().scroll_offset,
1601 0
1602 );
1603
1604 editor.interaction.dropdown_scroll(1000);
1606 let dd = editor.interaction.dropdown.as_ref().unwrap();
1607 let max_offset = dd.options.len().saturating_sub(dd.visible_count);
1608 assert_eq!(dd.scroll_offset, max_offset);
1609 }
1610
1611 #[test]
1612 fn dropdown_selected_item_visible_on_open() {
1613 let mut editor = make_editor_many_options();
1614 editor.params.set_normalized(0, 15.0 / 18.0);
1616
1617 let (dx, dy) = dropdown_center_many(&editor);
1618 editor.on_mouse_down(dx, dy);
1619 editor.on_mouse_up(dx, dy);
1620
1621 let dd = editor.interaction.dropdown.as_ref().unwrap();
1622 let selected = dd.selected;
1623 assert!(
1625 selected >= dd.scroll_offset && selected < dd.scroll_offset + dd.visible_count,
1626 "selected={selected} not in visible range {}..{}",
1627 dd.scroll_offset,
1628 dd.scroll_offset + dd.visible_count
1629 );
1630 }
1631
1632 #[test]
1633 fn dropdown_scroll_then_select_correct_index() {
1634 let mut editor = make_editor_many_options();
1635 let (dx, dy) = dropdown_center_many(&editor);
1636
1637 editor.on_mouse_down(dx, dy);
1638 editor.on_mouse_up(dx, dy);
1639
1640 editor.interaction.dropdown_scroll(3);
1642 assert_eq!(
1643 editor.interaction.dropdown.as_ref().unwrap().scroll_offset,
1644 3
1645 );
1646
1647 let dd = editor.interaction.dropdown.as_ref().unwrap();
1649 let (px, py, _, _) = dd.popup_rect;
1650 let item_h = 18.0f32;
1651 let padding = 4.0f32;
1652 let click_y = py + padding + item_h + item_h / 2.0; editor.on_mouse_down(px + 10.0, click_y);
1655 editor.on_mouse_up(px + 10.0, click_y);
1656
1657 assert!(!editor.interaction.dropdown_is_open());
1658 let norm = editor.params.get_normalized(0).unwrap();
1661 let expected = 4.0 / 19.0;
1662 assert!(
1663 (norm - expected).abs() < 0.01,
1664 "expected {expected:.4}, got {norm:.4}"
1665 );
1666 }
1667
1668 #[test]
1669 fn dropdown_click_different_dropdown_closes_first() {
1670 let mut editor = make_editor_two_dropdowns();
1671 let r0 = &editor.interaction.knob_regions[0];
1672 let r1 = &editor.interaction.knob_regions[1];
1673 let (ax, ay) = (r0.x + r0.w / 2.0, r0.y + r0.h / 2.0);
1674 let (bx, by) = (r1.x + r1.w / 2.0, r1.y + r1.h / 2.0);
1675
1676 editor.on_mouse_down(ax, ay);
1678 editor.on_mouse_up(ax, ay);
1679 assert!(editor.interaction.dropdown_is_open());
1680 assert_eq!(editor.interaction.dropdown.as_ref().unwrap().region_idx, 0);
1681
1682 editor.on_mouse_down(bx, by);
1684 editor.on_mouse_up(bx, by);
1685 assert!(editor.interaction.dropdown_is_open());
1686 assert_eq!(editor.interaction.dropdown.as_ref().unwrap().region_idx, 1);
1687 }
1688
1689 #[test]
1690 fn dropdown_hover_tracks_correct_option() {
1691 let mut editor = make_editor();
1692 let (dx, dy) = dropdown_center(&editor);
1693
1694 editor.on_mouse_down(dx, dy);
1695 editor.on_mouse_up(dx, dy);
1696
1697 let dd = editor.interaction.dropdown.as_ref().unwrap();
1698 let (px, py, pw, _) = dd.popup_rect;
1699 let item_h = 18.0f32;
1700 let padding = 4.0f32;
1701 let last_visible = dd.visible_count - 1;
1702
1703 let hover_y = py + padding + last_visible as f32 * item_h + item_h / 2.0;
1705 editor.on_mouse_moved(px + pw / 2.0, hover_y);
1706
1707 let dd = editor.interaction.dropdown.as_ref().unwrap();
1708 assert_eq!(
1709 dd.hover_option,
1710 Some(last_visible),
1711 "expected hover on last visible option"
1712 );
1713
1714 editor.on_mouse_moved(0.0, 0.0);
1716 let dd = editor.interaction.dropdown.as_ref().unwrap();
1717 assert_eq!(dd.hover_option, None, "hover should clear outside popup");
1718 }
1719
1720 #[test]
1721 fn dropdown_popup_within_window_bounds() {
1722 let mut editor = make_editor();
1724 let (dx, dy) = dropdown_center(&editor);
1725
1726 editor.on_mouse_down(dx, dy);
1727 editor.on_mouse_up(dx, dy);
1728
1729 let dd = editor.interaction.dropdown.as_ref().unwrap();
1730 let (px, py, pw, ph) = dd.popup_rect;
1731 let window_w = editor.layout.width() as f32;
1732 let window_h = editor.layout.height() as f32;
1733
1734 assert!(px >= 0.0, "popup left edge {px} < 0");
1735 assert!(py >= 0.0, "popup top edge {py} < 0");
1736 assert!(
1737 px + pw <= window_w + 1.0,
1738 "popup right {} > window {window_w}",
1739 px + pw
1740 );
1741 assert!(
1742 py + ph <= window_h + 1.0,
1743 "popup bottom {} > window {window_h}",
1744 py + ph
1745 );
1746 }
1747}