Skip to main content

truce_gui/
editor.rs

1//! Built-in editor using the CPU render backend.
2//!
3//! Renders parameter widgets via `RenderBackend`. Uses tiny-skia and blits
4//! RGBA pixels to a CALayer. For GPU rendering, see the `truce-gpu` crate
5//! which provides `GpuEditor` wrapping this editor with wgpu.
6
7use std::ffi::c_void;
8use std::sync::Arc;
9
10use truce_core::editor::{Editor, EditorContext, RawWindowHandle};
11use truce_params::Params;
12
13use crate::backend_cpu::CpuBackend;
14use crate::interaction::InteractionState;
15use crate::layout::{GridLayout, Layout, PluginLayout, compute_section_offsets,
16                     GRID_GAP, GRID_PADDING, GRID_HEADER_H, GRID_SECTION_H};
17use crate::platform::{PlatformView, ViewCallbacks};
18use crate::render::RenderBackend;
19use crate::theme::Theme;
20use crate::widgets;
21
22/// Built-in editor that renders parameter widgets to a pixel buffer.
23///
24/// Uses the CPU backend (tiny-skia) for software rasterization. When
25/// `open()` is called, creates a platform view and blits pixels at ~60fps.
26pub struct BuiltinEditor<P: Params> {
27    params: Arc<P>,
28    layout: Layout,
29    theme: Theme,
30    backend: Option<CpuBackend>,
31    interaction: InteractionState,
32    context: Option<EditorContext>,
33    view: Option<PlatformView>,
34    /// Leaked self-pointer for C callbacks. Cleaned up on close().
35    self_ptr: *mut c_void,
36}
37
38// Raw window handles and self_ptr are only accessed from the host UI thread.
39unsafe impl<P: Params> Send for BuiltinEditor<P> {}
40
41impl<P: Params + 'static> BuiltinEditor<P> {
42    pub fn new(params: Arc<P>, layout: PluginLayout) -> Self {
43        Self {
44            params,
45            layout: Layout::Rows(layout),
46            theme: Theme::dark(),
47            backend: None,
48            interaction: InteractionState::new(),
49            context: None,
50            view: None,
51            self_ptr: std::ptr::null_mut(),
52        }
53    }
54
55    pub fn new_with_layout(params: Arc<P>, layout: Layout) -> Self {
56        Self {
57            params,
58            layout,
59            theme: Theme::dark(),
60            backend: None,
61            interaction: InteractionState::new(),
62            context: None,
63            view: None,
64            self_ptr: std::ptr::null_mut(),
65        }
66    }
67
68    pub fn new_grid(params: Arc<P>, layout: GridLayout) -> Self {
69        Self {
70            params,
71            layout: Layout::Grid(layout),
72            theme: Theme::dark(),
73            backend: None,
74            interaction: InteractionState::new(),
75            context: None,
76            view: None,
77            self_ptr: std::ptr::null_mut(),
78        }
79    }
80
81    pub fn with_theme(mut self, theme: Theme) -> Self {
82        self.theme = theme;
83        self
84    }
85
86    /// Render the full UI to the internal CPU pixel buffer.
87    pub fn render(&mut self) {
88        let (w, h) = (self.layout.width(), self.layout.height());
89        let backend = self
90            .backend
91            .get_or_insert_with(|| CpuBackend::new(w, h).expect("Failed to create backend"));
92        // SAFETY: we split the borrow — backend is a separate field from layout/params/etc.
93        let backend_ptr = backend as *mut CpuBackend;
94        self.render_widgets(unsafe { &mut *backend_ptr });
95    }
96
97    /// Render all widgets to any `RenderBackend`.
98    fn render_widgets(&mut self, backend: &mut dyn RenderBackend) {
99        if matches!(self.layout, Layout::Grid(_)) {
100            self.render_grid_inner(backend);
101        } else {
102            self.render_rows_inner(backend);
103        }
104    }
105
106    fn render_rows_inner(&mut self, backend: &mut dyn RenderBackend) {
107        let pl = match &self.layout {
108            Layout::Rows(pl) => pl,
109            _ => return,
110        };
111        let w = pl.width;
112        let knob_size = pl.knob_size;
113        let title = pl.title;
114        let version = pl.version;
115
116        backend.clear(self.theme.background);
117        let theme = &self.theme;
118
119        widgets::draw_header(backend, 0.0, 0.0, w as f32, 30.0, title, version, theme);
120
121        let pl = match &self.layout {
122            Layout::Rows(pl) => pl,
123            _ => return,
124        };
125        let mut y = 35.0;
126        let mut render_widget_idx = 0usize;
127
128        for row in &pl.rows {
129            if let Some(label) = row.label {
130                widgets::draw_section_label(backend, 0.0, y, w as f32, label, theme);
131                y += 18.0;
132            }
133
134            let total_cols: u32 = row.knobs.iter().map(|k| k.span.max(1)).sum();
135            let total_w = total_cols as f32 * (knob_size + 10.0) - 10.0;
136            let start_x = (w as f32 - total_w) / 2.0;
137
138            let mut col = 0u32;
139            for knob_def in row.knobs.iter() {
140                let span = knob_def.span.max(1);
141                let x = start_x + col as f32 * (knob_size + 10.0);
142                let widget_w = span as f32 * (knob_size + 10.0) - 10.0;
143
144                let (normalized, value_text) = if let Some(ref ctx) = self.context {
145                    let n = (ctx.get_param)(knob_def.param_id) as f32;
146                    let t = (ctx.format_param)(knob_def.param_id);
147                    (n, t)
148                } else {
149                    let n = self.params.get_normalized(knob_def.param_id).unwrap_or(0.0) as f32;
150                    let p = self.params.get_plain(knob_def.param_id).unwrap_or(0.0);
151                    let t = self
152                        .params
153                        .format_value(knob_def.param_id, p)
154                        .unwrap_or_else(|| format!("{:.1}", p));
155                    (n, t)
156                };
157
158                let region_idx = render_widget_idx;
159                render_widget_idx += 1;
160                let is_hovered = self.interaction.hover_idx == Some(region_idx);
161
162                let wtype = resolve_widget_type(knob_def.widget, knob_def.param_id, &*self.params);
163
164                match wtype {
165                    widgets::WidgetType::Toggle => widgets::draw_toggle(
166                        backend, x, y, widget_w, knob_size,
167                        normalized, knob_def.label, &value_text,
168                        theme, is_hovered,
169                    ),
170                    widgets::WidgetType::Slider => widgets::draw_slider(
171                        backend, x, y, widget_w, knob_size,
172                        normalized, knob_def.label, &value_text,
173                        theme, is_hovered,
174                    ),
175                    widgets::WidgetType::Selector => widgets::draw_selector(
176                        backend, x, y, widget_w, knob_size,
177                        normalized, knob_def.label, &value_text,
178                        theme, is_hovered,
179                    ),
180                    widgets::WidgetType::Meter => {
181                        let default_ids = vec![knob_def.param_id];
182                        let ids = knob_def.meter_ids.as_deref()
183                            .unwrap_or(&default_ids);
184                        let levels: Vec<f32> = if let Some(ref ctx) = self.context {
185                            ids.iter().map(|&id| (ctx.get_meter)(id)).collect()
186                        } else {
187                            vec![0.0; ids.len()]
188                        };
189                        widgets::draw_meter(
190                            backend, x, y, widget_w, knob_size,
191                            &levels, knob_def.label, theme,
192                        );
193                    },
194                    widgets::WidgetType::XYPad => {
195                        let val_y_id = knob_def.param_id_y.unwrap_or(knob_def.param_id);
196                        let (vx, vy) = if let Some(ref ctx) = self.context {
197                            ((ctx.get_param)(knob_def.param_id) as f32,
198                             (ctx.get_param)(val_y_id) as f32)
199                        } else {
200                            (self.params.get_normalized(knob_def.param_id).unwrap_or(0.0) as f32,
201                             self.params.get_normalized(val_y_id).unwrap_or(0.0) as f32)
202                        };
203                        let infos = self.params.param_infos();
204                        let x_name = infos.iter().find(|i| i.id == knob_def.param_id)
205                            .map(|i| i.name).unwrap_or(knob_def.label);
206                        let y_name = infos.iter().find(|i| i.id == val_y_id)
207                            .map(|i| i.name).unwrap_or("");
208                        widgets::draw_xy_pad(
209                            backend, x, y, widget_w, knob_size,
210                            vx, vy, x_name, y_name, theme, is_hovered,
211                        );
212                    },
213                    widgets::WidgetType::Knob => widgets::draw_knob(
214                        backend, x, y, knob_size, normalized,
215                        knob_def.label, &value_text, theme, is_hovered,
216                    ),
217                }
218                col += span;
219            }
220
221            y += knob_size + 30.0;
222        }
223    }
224
225    fn render_grid_inner(&mut self, backend: &mut dyn RenderBackend) {
226        let grid = match &self.layout {
227            Layout::Grid(g) => g,
228            _ => return,
229        };
230        let w = grid.width;
231        let title = grid.title;
232        let version = grid.version;
233
234        backend.clear(self.theme.background);
235        let theme = &self.theme;
236
237        widgets::draw_header(backend, 0.0, 0.0, w as f32, 30.0, title, version, theme);
238
239        let grid = match &self.layout {
240            Layout::Grid(g) => g,
241            _ => return,
242        };
243
244        let section_offsets = compute_section_offsets(grid);
245
246        // Section labels
247        for &(row_idx, label) in &grid.sections {
248            let y = GRID_HEADER_H + GRID_PADDING
249                + row_idx as f32 * (grid.cell_size + GRID_GAP)
250                + section_offsets[row_idx as usize]
251                - GRID_SECTION_H;
252            widgets::draw_section_label(backend, 0.0, y, w as f32, label, theme);
253        }
254
255        // Widgets
256        for (idx, gw) in grid.widgets.iter().enumerate() {
257            let x = GRID_PADDING + gw.col as f32 * (grid.cell_size + GRID_GAP);
258            let y = GRID_HEADER_H + GRID_PADDING
259                + gw.row as f32 * (grid.cell_size + GRID_GAP)
260                + section_offsets[gw.row as usize];
261            let widget_w = gw.col_span as f32 * (grid.cell_size + GRID_GAP) - GRID_GAP;
262            let widget_h = gw.row_span as f32 * (grid.cell_size + GRID_GAP) - GRID_GAP;
263
264            let (normalized, value_text) = if let Some(ref ctx) = self.context {
265                let n = (ctx.get_param)(gw.param_id) as f32;
266                let t = (ctx.format_param)(gw.param_id);
267                (n, t)
268            } else {
269                let n = self.params.get_normalized(gw.param_id).unwrap_or(0.0) as f32;
270                let p = self.params.get_plain(gw.param_id).unwrap_or(0.0);
271                let t = self
272                    .params
273                    .format_value(gw.param_id, p)
274                    .unwrap_or_else(|| format!("{:.1}", p));
275                (n, t)
276            };
277
278            let is_hovered = self.interaction.hover_idx == Some(idx);
279            let wtype = resolve_widget_type(gw.widget, gw.param_id, &*self.params);
280
281            match wtype {
282                widgets::WidgetType::Toggle => widgets::draw_toggle(
283                    backend, x, y, widget_w, widget_h,
284                    normalized, gw.label, &value_text, theme, is_hovered,
285                ),
286                widgets::WidgetType::Slider => widgets::draw_slider(
287                    backend, x, y, widget_w, widget_h,
288                    normalized, gw.label, &value_text, theme, is_hovered,
289                ),
290                widgets::WidgetType::Selector => widgets::draw_selector(
291                    backend, x, y, widget_w, widget_h,
292                    normalized, gw.label, &value_text, theme, is_hovered,
293                ),
294                widgets::WidgetType::Meter => {
295                    let default_ids = vec![gw.param_id];
296                    let ids = gw.meter_ids.as_deref().unwrap_or(&default_ids);
297                    let levels: Vec<f32> = if let Some(ref ctx) = self.context {
298                        ids.iter().map(|&id| (ctx.get_meter)(id)).collect()
299                    } else {
300                        vec![0.0; ids.len()]
301                    };
302                    widgets::draw_meter(
303                        backend, x, y, widget_w, widget_h,
304                        &levels, gw.label, theme,
305                    );
306                },
307                widgets::WidgetType::XYPad => {
308                    let val_y_id = gw.param_id_y.unwrap_or(gw.param_id);
309                    let (vx, vy) = if let Some(ref ctx) = self.context {
310                        ((ctx.get_param)(gw.param_id) as f32,
311                         (ctx.get_param)(val_y_id) as f32)
312                    } else {
313                        (self.params.get_normalized(gw.param_id).unwrap_or(0.0) as f32,
314                         self.params.get_normalized(val_y_id).unwrap_or(0.0) as f32)
315                    };
316                    let infos = self.params.param_infos();
317                    let x_name = infos.iter().find(|i| i.id == gw.param_id)
318                        .map(|i| i.name).unwrap_or(gw.label);
319                    let y_name = infos.iter().find(|i| i.id == val_y_id)
320                        .map(|i| i.name).unwrap_or("");
321                    widgets::draw_xy_pad(
322                        backend, x, y, widget_w, widget_h,
323                        vx, vy, x_name, y_name, theme, is_hovered,
324                    );
325                },
326                widgets::WidgetType::Knob => {
327                    let knob_size = widget_w.min(widget_h);
328                    let kx = x + (widget_w - knob_size) / 2.0;
329                    let ky = y + (widget_h - knob_size) / 2.0;
330                    widgets::draw_knob(
331                        backend, kx, ky, knob_size, normalized,
332                        gw.label, &value_text, theme, is_hovered,
333                    );
334                },
335            }
336        }
337    }
338
339    /// Get the raw pixel data after rendering (RGBA premultiplied).
340    pub fn pixel_data(&self) -> Option<&[u8]> {
341        self.backend.as_ref().map(|b| b.data())
342    }
343
344    /// Get the KnobDef at a flattened index (Rows layout only).
345    fn knob_def_at(&self, idx: usize) -> Option<&crate::layout::KnobDef> {
346        if let Layout::Rows(pl) = &self.layout {
347            let mut i = 0;
348            for row in &pl.rows {
349                for kd in &row.knobs {
350                    if i == idx { return Some(kd); }
351                    i += 1;
352                }
353            }
354        }
355        None
356    }
357
358    /// Get the Y-axis param ID for an XY pad at the given region index.
359    fn param_id_y_at(&self, idx: usize) -> Option<u32> {
360        match &self.layout {
361            Layout::Rows(_) => self.knob_def_at(idx).and_then(|kd| kd.param_id_y),
362            Layout::Grid(g) => g.widgets.get(idx).and_then(|w| w.param_id_y),
363        }
364    }
365
366    // --- Public API for external backends (truce-gpu) ---
367
368    /// Set the editor context (host callbacks) without opening the CPU view.
369    pub fn set_context(&mut self, context: EditorContext) {
370        self.context = Some(context);
371        match &self.layout {
372            Layout::Rows(pl) => self.interaction.build_regions(pl),
373            Layout::Grid(gl) => self.interaction.build_regions_grid(gl),
374        }
375    }
376
377    /// Render all widgets to an external `RenderBackend`.
378    ///
379    /// Used by `truce-gpu` to draw through the GPU backend instead of
380    /// the internal CPU backend.
381    pub fn render_to(&mut self, backend: &mut dyn RenderBackend) {
382        unsafe { update_interaction(self) };
383        self.render_widgets(backend);
384    }
385
386    // --- Mouse event handlers (public for external backends) ---
387
388    pub fn on_mouse_down(&mut self, x: f32, y: f32) {
389        if let Some(idx) = self.interaction.hit_test(x, y) {
390            let param_id = self.interaction.knob_regions[idx].param_id;
391            let wtype = self.interaction.widget_type_at(idx);
392            if wtype == Some(crate::widgets::WidgetType::Toggle) {
393                let norm = self.params.get_normalized(param_id).unwrap_or(0.0);
394                let new_norm = if norm > 0.5 { 0.0 } else { 1.0 };
395                self.params.set_normalized(param_id, new_norm);
396                if let Some(ref ctx) = self.context {
397                    (ctx.begin_edit)(param_id);
398                    (ctx.set_param)(param_id, new_norm);
399                    (ctx.end_edit)(param_id);
400                }
401            } else if wtype == Some(crate::widgets::WidgetType::Selector) {
402                if let Some(info) = self.params.param_infos().into_iter().find(|i| i.id == param_id) {
403                    let plain = self.params.get_plain(param_id).unwrap_or(0.0);
404                    let max = info.range.max();
405                    let next = if plain >= max { 0.0 } else { plain + 1.0 };
406                    let new_norm = info.range.normalize(next);
407                    self.params.set_normalized(param_id, new_norm);
408                    if let Some(ref ctx) = self.context {
409                        (ctx.begin_edit)(param_id);
410                        (ctx.set_param)(param_id, new_norm);
411                        (ctx.end_edit)(param_id);
412                    }
413                }
414            } else {
415                let norm = self.params.get_normalized(param_id).unwrap_or(0.0);
416                self.interaction.begin_drag(idx, norm, y);
417                if let Some(ref ctx) = self.context {
418                    (ctx.begin_edit)(param_id);
419                    if wtype == Some(crate::widgets::WidgetType::XYPad) {
420                        if let Some(y_id) = self.param_id_y_at(idx) {
421                            (ctx.begin_edit)(y_id);
422                        }
423                    }
424                }
425            }
426        }
427    }
428
429    pub fn on_mouse_dragged(&mut self, x: f32, y: f32) {
430        if let Some(drag) = &self.interaction.dragging {
431            if drag.widget_type == crate::widgets::WidgetType::XYPad {
432                let pad_margin = 4.0;
433                let label_h = 18.0;
434                let pad_x = drag.region_x + pad_margin;
435                let pad_w = drag.region_w - pad_margin * 2.0;
436                let pad_y_start = drag.region_y + pad_margin;
437                let pad_h = drag.region_h - pad_margin * 2.0 - label_h;
438
439                let norm_x = ((x - pad_x) / pad_w).clamp(0.0, 1.0) as f64;
440                let norm_y = (1.0 - (y - pad_y_start) / pad_h).clamp(0.0, 1.0) as f64;
441
442                let param_id = drag.param_id;
443                let region_idx = drag.region_idx;
444                self.params.set_normalized(param_id, norm_x);
445                if let Some(ref ctx) = self.context {
446                    (ctx.set_param)(param_id, norm_x);
447                }
448
449                if let Some(y_id) = self.param_id_y_at(region_idx) {
450                    self.params.set_normalized(y_id, norm_y);
451                    if let Some(ref ctx) = self.context {
452                        (ctx.set_param)(y_id, norm_y);
453                    }
454                }
455            } else if drag.widget_type == crate::widgets::WidgetType::Slider {
456                if let Some((param_id, new_norm)) = self.interaction.update_slider_drag(x) {
457                    self.params.set_normalized(param_id, new_norm);
458                    if let Some(ref ctx) = self.context {
459                        (ctx.set_param)(param_id, new_norm);
460                    }
461                }
462            } else {
463                if let Some((param_id, new_norm)) = self.interaction.update_drag(y) {
464                    self.params.set_normalized(param_id, new_norm);
465                    if let Some(ref ctx) = self.context {
466                        (ctx.set_param)(param_id, new_norm);
467                    }
468                }
469            }
470        }
471    }
472
473    pub fn on_mouse_up(&mut self, _x: f32, _y: f32) {
474        if let Some(drag) = &self.interaction.dragging {
475            let param_id = drag.param_id;
476            let was_xy = drag.widget_type == crate::widgets::WidgetType::XYPad;
477            let region_idx = drag.region_idx;
478            self.interaction.end_drag();
479            if let Some(ref ctx) = self.context {
480                (ctx.end_edit)(param_id);
481                if was_xy {
482                    if let Some(y_id) = self.param_id_y_at(region_idx) {
483                        (ctx.end_edit)(y_id);
484                    }
485                }
486            }
487        }
488    }
489
490    pub fn on_double_click(&mut self, x: f32, y: f32) {
491        if let Some(idx) = self.interaction.hit_test(x, y) {
492            let param_id = self.interaction.knob_regions[idx].param_id;
493            // Reset to default value
494            let infos = self.params.param_infos();
495            if let Some(info) = infos.iter().find(|i| i.id == param_id) {
496                let default_norm = info.range.normalize(info.default_plain);
497                self.params.set_normalized(param_id, default_norm);
498                if let Some(ref ctx) = self.context {
499                    (ctx.begin_edit)(param_id);
500                    (ctx.set_param)(param_id, default_norm);
501                    (ctx.end_edit)(param_id);
502                }
503            }
504        }
505    }
506
507    pub fn on_scroll(&mut self, x: f32, y: f32, delta_y: f32) {
508        if let Some(idx) = self.interaction.hit_test(x, y) {
509            let param_id = self.interaction.knob_regions[idx].param_id;
510            let norm = self.params.get_normalized(param_id).unwrap_or(0.0);
511            let step = delta_y as f64 / 200.0; // 200 pixels of scroll = full range
512            let new_norm = (norm + step).clamp(0.0, 1.0);
513            self.params.set_normalized(param_id, new_norm);
514            if let Some(ref ctx) = self.context {
515                (ctx.begin_edit)(param_id);
516                (ctx.set_param)(param_id, new_norm);
517                (ctx.end_edit)(param_id);
518            }
519        }
520    }
521
522    pub fn on_mouse_moved(&mut self, x: f32, y: f32) -> bool {
523        self.interaction.hover_idx = self.interaction.hit_test(x, y);
524        self.interaction.hover_idx.is_some()
525    }
526}
527
528// ---------------------------------------------------------------------------
529// C callbacks — thin wrappers that cast the context pointer back to &mut Self
530// ---------------------------------------------------------------------------
531
532/// Update interaction regions and live param values.
533///
534/// # Safety
535/// The editor must be valid and not concurrently accessed.
536pub unsafe fn update_interaction<P: Params + 'static>(editor: &mut BuiltinEditor<P>) {
537    match &editor.layout {
538        Layout::Rows(pl) => {
539            editor.interaction.build_regions(pl);
540            let mut flat_idx = 0usize;
541            for row in &pl.rows {
542                for knob_def in &row.knobs {
543                    if let Some(region) = editor.interaction.knob_regions.get_mut(flat_idx) {
544                        region.widget_type = resolve_widget_type(
545                            knob_def.widget, knob_def.param_id, &*editor.params,
546                        );
547                    }
548                    flat_idx += 1;
549                }
550            }
551        }
552        Layout::Grid(gl) => {
553            editor.interaction.build_regions_grid(gl);
554            for (idx, gw) in gl.widgets.iter().enumerate() {
555                if let Some(region) = editor.interaction.knob_regions.get_mut(idx) {
556                    region.widget_type = resolve_widget_type(
557                        gw.widget, gw.param_id, &*editor.params,
558                    );
559                }
560            }
561        }
562    }
563    for region in &mut editor.interaction.knob_regions {
564        if let Some(ref ctx) = editor.context {
565            region.normalized_value = (ctx.get_param)(region.param_id) as f32;
566        } else {
567            region.normalized_value =
568                editor.params.get_normalized(region.param_id).unwrap_or(0.0) as f32;
569        }
570    }
571}
572
573unsafe extern "C" fn cb_render<P: Params + 'static>(
574    ctx: *mut c_void,
575    out_w: *mut u32,
576    out_h: *mut u32,
577) -> *const u8 {
578    let editor = &mut *(ctx as *mut BuiltinEditor<P>);
579    update_interaction(editor);
580    editor.render();
581    let backend = match editor.backend.as_ref() {
582        Some(b) => b,
583        None => return std::ptr::null(),
584    };
585    *out_w = backend.width();
586    *out_h = backend.height();
587    backend.data().as_ptr()
588}
589
590unsafe extern "C" fn cb_mouse_down<P: Params + 'static>(ctx: *mut c_void, x: f32, y: f32) {
591    let editor = &mut *(ctx as *mut BuiltinEditor<P>);
592    editor.on_mouse_down(x, y);
593}
594
595unsafe extern "C" fn cb_mouse_dragged<P: Params + 'static>(ctx: *mut c_void, x: f32, y: f32) {
596    let editor = &mut *(ctx as *mut BuiltinEditor<P>);
597    editor.on_mouse_dragged(x, y);
598}
599
600unsafe extern "C" fn cb_mouse_up<P: Params + 'static>(ctx: *mut c_void, x: f32, y: f32) {
601    let editor = &mut *(ctx as *mut BuiltinEditor<P>);
602    editor.on_mouse_up(x, y);
603}
604
605unsafe extern "C" fn cb_scroll<P: Params + 'static>(
606    ctx: *mut c_void,
607    x: f32,
608    y: f32,
609    delta_y: f32,
610) {
611    let editor = &mut *(ctx as *mut BuiltinEditor<P>);
612    editor.on_scroll(x, y, delta_y);
613}
614
615unsafe extern "C" fn cb_double_click<P: Params + 'static>(ctx: *mut c_void, x: f32, y: f32) {
616    let editor = &mut *(ctx as *mut BuiltinEditor<P>);
617    editor.on_double_click(x, y);
618}
619
620unsafe extern "C" fn cb_mouse_moved<P: Params + 'static>(ctx: *mut c_void, x: f32, y: f32) -> u8 {
621    let editor = &mut *(ctx as *mut BuiltinEditor<P>);
622    editor.on_mouse_moved(x, y) as u8
623}
624
625// ---------------------------------------------------------------------------
626// Editor trait implementation
627// ---------------------------------------------------------------------------
628
629/// Resolve widget type: explicit override > auto-detect from param range.
630fn resolve_widget_type<P: Params>(
631    widget: Option<crate::layout::WidgetKind>,
632    param_id: u32,
633    params: &P,
634) -> widgets::WidgetType {
635    match widget {
636        Some(crate::layout::WidgetKind::Knob) => widgets::WidgetType::Knob,
637        Some(crate::layout::WidgetKind::Slider) => widgets::WidgetType::Slider,
638        Some(crate::layout::WidgetKind::Toggle) => widgets::WidgetType::Toggle,
639        Some(crate::layout::WidgetKind::Selector) => widgets::WidgetType::Selector,
640        Some(crate::layout::WidgetKind::Meter) => widgets::WidgetType::Meter,
641        Some(crate::layout::WidgetKind::XYPad) => widgets::WidgetType::XYPad,
642        None => {
643            let param_info = params.param_infos().into_iter()
644                .find(|i| i.id == param_id);
645            match param_info.as_ref().map(|i| &i.range) {
646                Some(truce_params::ParamRange::Discrete { min: 0, max: 1 }) => widgets::WidgetType::Toggle,
647                Some(truce_params::ParamRange::Enum { .. }) => widgets::WidgetType::Selector,
648                _ => widgets::WidgetType::Knob,
649            }
650        }
651    }
652}
653
654impl<P: Params + 'static> Editor for BuiltinEditor<P> {
655    fn size(&self) -> (u32, u32) {
656        (self.layout.width(), self.layout.height())
657    }
658
659    fn open(&mut self, parent: RawWindowHandle, context: EditorContext) {
660        let (w, h) = self.size();
661        self.backend = CpuBackend::new(w, h);
662        self.context = Some(context);
663
664        // Build interaction regions
665        match &self.layout {
666            Layout::Rows(pl) => self.interaction.build_regions(pl),
667            Layout::Grid(gl) => self.interaction.build_regions_grid(gl),
668        }
669
670        // Render initial frame
671        self.render();
672
673        // Create platform view if we have a parent window
674        let parent_ptr = match parent {
675            RawWindowHandle::AppKit(ptr) => ptr,
676            #[allow(unused)]
677            _ => std::ptr::null_mut(),
678        };
679
680        if !parent_ptr.is_null() {
681            let self_ptr = self as *mut BuiltinEditor<P> as *mut c_void;
682            self.self_ptr = self_ptr;
683
684            let callbacks = ViewCallbacks {
685                render: Some(cb_render::<P>),
686                mouse_down: Some(cb_mouse_down::<P>),
687                mouse_dragged: Some(cb_mouse_dragged::<P>),
688                mouse_up: Some(cb_mouse_up::<P>),
689                scroll: Some(cb_scroll::<P>),
690                double_click: Some(cb_double_click::<P>),
691                mouse_moved: Some(cb_mouse_moved::<P>),
692            };
693
694            self.view = unsafe { PlatformView::new(parent_ptr, w, h, self_ptr, &callbacks) };
695        }
696    }
697
698    fn close(&mut self) {
699        self.view = None;
700        self.context = None;
701        self.backend = None;
702        self.self_ptr = std::ptr::null_mut();
703    }
704
705    fn idle(&mut self) {
706        // Platform view handles its own repaint timer.
707        // If no platform view (standalone mode), render for external consumption.
708        if self.view.is_none() {
709            self.render();
710        }
711    }
712}