Skip to main content

truce_gui/
interaction.rs

1//! Mouse interaction for GUI widgets.
2//!
3//! Tracks widget hit regions and maps mouse drags to parameter value changes.
4
5use crate::layout::{GridLayout, PluginLayout, compute_section_offsets,
6                     GRID_GAP, GRID_PADDING, GRID_HEADER_H};
7use crate::widgets::WidgetType;
8
9/// A widget's hit region on screen.
10#[derive(Clone, Debug)]
11pub struct WidgetRegion {
12    pub param_id: u32,
13    pub widget_type: WidgetType,
14    pub x: f32,
15    pub y: f32,
16    pub w: f32,
17    pub h: f32,
18    /// Center x/y and radius for knob (circular hit test).
19    pub cx: f32,
20    pub cy: f32,
21    pub radius: f32,
22    pub normalized_value: f32,
23}
24
25/// Backward-compatible alias.
26pub type KnobRegion = WidgetRegion;
27
28/// Tracks the current mouse interaction state.
29pub struct InteractionState {
30    pub knob_regions: Vec<WidgetRegion>,
31    pub dragging: Option<DragState>,
32    /// Region index under the cursor (for hover highlight).
33    pub hover_idx: Option<usize>,
34}
35
36pub struct DragState {
37    pub region_idx: usize,
38    pub param_id: u32,
39    pub start_value: f64,
40    pub start_y: f32,
41    pub widget_type: WidgetType,
42    pub region_x: f32,
43    pub region_y: f32,
44    pub region_w: f32,
45    pub region_h: f32,
46}
47
48impl Default for InteractionState {
49    fn default() -> Self {
50        Self::new()
51    }
52}
53
54impl InteractionState {
55    pub fn new() -> Self {
56        Self {
57            knob_regions: Vec::new(),
58            dragging: None,
59            hover_idx: None,
60        }
61    }
62
63    /// Rebuild hit regions from the layout. Call after render.
64    pub fn build_regions(&mut self, layout: &PluginLayout) {
65        self.knob_regions.clear();
66
67        let knob_size = layout.knob_size;
68        let mut y = 35.0f32;
69
70        for row in &layout.rows {
71            if row.label.is_some() {
72                y += 18.0;
73            }
74
75            let total_cols: u32 = row.knobs.iter().map(|k| k.span.max(1)).sum();
76            let total_w = total_cols as f32 * (knob_size + 10.0) - 10.0;
77            let start_x = (layout.width as f32 - total_w) / 2.0;
78
79            let mut col = 0u32;
80            for knob_def in row.knobs.iter() {
81                let span = knob_def.span.max(1);
82                let x = start_x + col as f32 * (knob_size + 10.0);
83                let widget_w = span as f32 * (knob_size + 10.0) - 10.0;
84                let cx = x + widget_w / 2.0;
85                let cy = y + knob_size / 2.0 - 8.0;
86                let radius = knob_size / 2.0 - 6.0;
87
88                self.knob_regions.push(WidgetRegion {
89                    param_id: knob_def.param_id,
90                    widget_type: WidgetType::Knob,
91                    x,
92                    y,
93                    w: widget_w,
94                    h: knob_size,
95                    cx,
96                    cy,
97                    radius,
98                    normalized_value: 0.0,
99                });
100                col += span;
101            }
102
103            y += knob_size + 30.0;
104        }
105    }
106
107    /// Check if a mouse position hits a widget. Returns the region index if so.
108    pub fn hit_test(&self, mx: f32, my: f32) -> Option<usize> {
109        for (idx, region) in self.knob_regions.iter().enumerate() {
110            match region.widget_type {
111                WidgetType::Knob => {
112                    let dx = mx - region.cx;
113                    let dy = my - region.cy;
114                    if dx * dx + dy * dy <= region.radius * region.radius {
115                        return Some(idx);
116                    }
117                }
118                WidgetType::Meter => continue,
119                WidgetType::Slider | WidgetType::Toggle | WidgetType::Selector | WidgetType::XYPad => {
120                    if mx >= region.x && mx <= region.x + region.w
121                        && my >= region.y && my <= region.y + region.h
122                    {
123                        return Some(idx);
124                    }
125                }
126            }
127        }
128        None
129    }
130
131    /// Get the widget type by region index.
132    pub fn widget_type_at(&self, idx: usize) -> Option<WidgetType> {
133        self.knob_regions.get(idx).map(|r| r.widget_type)
134    }
135
136    /// Get the region by index.
137    pub fn region_at(&self, idx: usize) -> Option<&WidgetRegion> {
138        self.knob_regions.get(idx)
139    }
140
141    /// Begin a drag on a widget by region index.
142    pub fn begin_drag(&mut self, idx: usize, current_normalized: f64, mouse_y: f32) {
143        let region = match self.knob_regions.get(idx) {
144            Some(r) => r,
145            None => return,
146        };
147        let param_id = region.param_id;
148        let wtype = region.widget_type;
149        self.dragging = Some(DragState {
150            region_idx: idx,
151            param_id,
152            start_value: current_normalized,
153            start_y: mouse_y,
154            widget_type: wtype,
155            region_x: region.x,
156            region_y: region.y,
157            region_w: region.w,
158            region_h: region.h,
159        });
160    }
161
162    /// Update during a drag. Returns (param_id, new_normalized_value) if dragging.
163    pub fn update_drag(&self, mouse_y: f32) -> Option<(u32, f64)> {
164        let drag = self.dragging.as_ref()?;
165        let dy = drag.start_y - mouse_y;
166        let delta = dy as f64 / 200.0;
167        let new_value = (drag.start_value + delta).clamp(0.0, 1.0);
168        Some((drag.param_id, new_value))
169    }
170
171    /// Update during a horizontal slider drag. Returns (param_id, new_value).
172    pub fn update_slider_drag(&self, mouse_x: f32) -> Option<(u32, f64)> {
173        let drag = self.dragging.as_ref()?;
174        let margin = 6.0;
175        let rel = (mouse_x - drag.region_x - margin) / (drag.region_w - margin * 2.0);
176        let new_value = (rel as f64).clamp(0.0, 1.0);
177        Some((drag.param_id, new_value))
178    }
179
180    /// End a drag.
181    pub fn end_drag(&mut self) {
182        self.dragging = None;
183    }
184
185    /// Rebuild hit regions from a grid layout.
186    pub fn build_regions_grid(&mut self, layout: &GridLayout) {
187        self.knob_regions.clear();
188
189        let section_offsets = compute_section_offsets(layout);
190
191        for gw in &layout.widgets {
192            let x = GRID_PADDING + gw.col as f32 * (layout.cell_size + GRID_GAP);
193            let y = GRID_HEADER_H + GRID_PADDING
194                + gw.row as f32 * (layout.cell_size + GRID_GAP)
195                + section_offsets[gw.row as usize];
196            let w = gw.col_span as f32 * (layout.cell_size + GRID_GAP) - GRID_GAP;
197            let h = gw.row_span as f32 * (layout.cell_size + GRID_GAP) - GRID_GAP;
198            let cx = x + w / 2.0;
199            let cy = y + h / 2.0 - 8.0;
200            let radius = w.min(h) / 2.0 - 6.0;
201
202            self.knob_regions.push(WidgetRegion {
203                param_id: gw.param_id,
204                widget_type: WidgetType::Knob, // set later by editor
205                x, y, w, h,
206                cx, cy, radius,
207                normalized_value: 0.0,
208            });
209        }
210    }
211}