Skip to main content

agg_rust/
ctrl.rs

1//! Interactive UI controls rendered via AGG's rendering pipeline.
2//!
3//! Port of `ctrl/agg_slider_ctrl.h`, `ctrl/agg_cbox_ctrl.h`, `ctrl/agg_rbox_ctrl.h`.
4//!
5//! Controls are vertex sources with multiple colored paths. Use `render_ctrl()`
6//! to render them into a renderer_base, which iterates paths and colors.
7//!
8//! In the original C++ library, controls handle mouse interaction directly.
9//! In our WASM demos, the JS sidebar handles interaction and passes values
10//! as params; these controls render the visual representation on the canvas.
11
12use crate::basics::{
13    is_stop, VertexSource, PATH_CMD_LINE_TO, PATH_CMD_MOVE_TO, PATH_CMD_STOP,
14};
15use crate::bspline::Bspline;
16use crate::color::Rgba8;
17use crate::conv_stroke::ConvStroke;
18use crate::ellipse::Ellipse;
19use crate::gsv_text::GsvText;
20use crate::math_stroke::{LineCap, LineJoin};
21use crate::path_storage::PathStorage;
22use crate::pixfmt_rgba::PixfmtRgba32;
23use crate::rasterizer_scanline_aa::RasterizerScanlineAa;
24use crate::renderer_base::RendererBase;
25use crate::renderer_scanline::render_scanlines_aa_solid;
26use crate::scanline_u::ScanlineU8;
27
28// ============================================================================
29// render_ctrl — render any control with its multi-path color scheme
30// ============================================================================
31
32/// Render a control by iterating its paths and rendering each with its color.
33///
34/// Port of C++ `render_ctrl()` template function.
35pub fn render_ctrl(
36    ras: &mut RasterizerScanlineAa,
37    sl: &mut ScanlineU8,
38    ren: &mut RendererBase<PixfmtRgba32>,
39    ctrl: &mut dyn Ctrl,
40) {
41    for i in 0..ctrl.num_paths() {
42        ras.reset();
43        ras.add_path(ctrl, i);
44        render_scanlines_aa_solid(ras, sl, ren, &ctrl.color(i));
45    }
46}
47
48/// Trait for AGG controls that can be rendered as multi-path vertex sources.
49pub trait Ctrl: VertexSource {
50    fn num_paths(&self) -> u32;
51    fn color(&self, path_id: u32) -> Rgba8;
52}
53
54// ============================================================================
55// C-style sprintf format helper
56// ============================================================================
57
58/// Simple C-style sprintf emulation for format strings containing one `%` specifier.
59///
60/// Supports patterns like `%f`, `%.2f`, `%1.0f`, `%5.3f`, `%d`, `%3d`, etc.
61/// Only the first `%` specifier is replaced; the rest of the string is kept as-is.
62fn sprintf_format(fmt: &str, value: f64) -> String {
63    let mut result = String::with_capacity(fmt.len() + 16);
64    let bytes = fmt.as_bytes();
65    let mut i = 0;
66    let mut formatted = false;
67
68    while i < bytes.len() {
69        if !formatted && bytes[i] == b'%' {
70            // Parse the format specifier: %[width][.precision][type]
71            let start = i;
72            i += 1; // skip '%'
73            if i < bytes.len() && bytes[i] == b'%' {
74                // Literal %%
75                result.push('%');
76                i += 1;
77                continue;
78            }
79            // Skip flags (-, +, 0, space)
80            while i < bytes.len() && matches!(bytes[i], b'-' | b'+' | b'0' | b' ') {
81                i += 1;
82            }
83            // Skip width digits
84            while i < bytes.len() && bytes[i].is_ascii_digit() {
85                i += 1;
86            }
87            // Parse optional .precision
88            let mut precision: Option<usize> = None;
89            if i < bytes.len() && bytes[i] == b'.' {
90                i += 1;
91                let prec_start = i;
92                while i < bytes.len() && bytes[i].is_ascii_digit() {
93                    i += 1;
94                }
95                if i > prec_start {
96                    precision = fmt[prec_start..i].parse().ok();
97                } else {
98                    precision = Some(0);
99                }
100            }
101            // Parse type character
102            if i < bytes.len() {
103                match bytes[i] {
104                    b'f' | b'F' => {
105                        let p = precision.unwrap_or(6);
106                        result.push_str(&format!("{:.prec$}", value, prec = p));
107                        i += 1;
108                        formatted = true;
109                    }
110                    b'd' | b'i' => {
111                        result.push_str(&format!("{}", value as i64));
112                        i += 1;
113                        formatted = true;
114                    }
115                    b'e' | b'E' => {
116                        let p = precision.unwrap_or(6);
117                        result.push_str(&format!("{:.prec$e}", value, prec = p));
118                        i += 1;
119                        formatted = true;
120                    }
121                    b'g' | b'G' => {
122                        let p = precision.unwrap_or(6);
123                        // Simple %g: use fixed if short enough, else scientific
124                        let s = format!("{:.prec$}", value, prec = p);
125                        let e = format!("{:.prec$e}", value, prec = p);
126                        result.push_str(if s.len() <= e.len() { &s } else { &e });
127                        i += 1;
128                        formatted = true;
129                    }
130                    _ => {
131                        // Unknown specifier — output literally
132                        result.push_str(&fmt[start..=i]);
133                        i += 1;
134                    }
135                }
136            } else {
137                // Trailing '%' — output literally
138                result.push('%');
139            }
140        } else {
141            result.push(bytes[i] as char);
142            i += 1;
143        }
144    }
145    result
146}
147
148// ============================================================================
149// SliderCtrl — horizontal value slider
150// ============================================================================
151
152/// Horizontal slider control rendered via AGG.
153///
154/// Renders 6 paths: background, triangle indicator, label text,
155/// pointer preview, active pointer, and step markers.
156///
157/// Port of C++ `slider_ctrl_impl`.
158pub struct SliderCtrl {
159    // Bounds
160    x1: f64,
161    y1: f64,
162    x2: f64,
163    y2: f64,
164    // Inner slider bounds
165    xs1: f64,
166    ys1: f64,
167    xs2: f64,
168    ys2: f64,
169    // Value (normalized 0..1)
170    value: f64,
171    preview_value: f64,
172    min: f64,
173    max: f64,
174    // Text
175    label: String,
176    // Visual
177    border_width: f64,
178    border_extra: f64,
179    text_thickness: f64,
180    num_steps: u32,
181    descending: bool,
182    // Colors (6 paths)
183    colors: [Rgba8; 6],
184    // Rendering state
185    vertices: Vec<(f64, f64, u32)>,
186    vertex_idx: usize,
187}
188
189impl SliderCtrl {
190    pub fn new(x1: f64, y1: f64, x2: f64, y2: f64) -> Self {
191        let border_extra = (y2 - y1) / 2.0;
192        let mut s = Self {
193            x1,
194            y1,
195            x2,
196            y2,
197            xs1: 0.0,
198            ys1: 0.0,
199            xs2: 0.0,
200            ys2: 0.0,
201            value: 0.5,
202            preview_value: 0.5,
203            min: 0.0,
204            max: 1.0,
205            label: String::new(),
206            border_width: 1.0,
207            border_extra,
208            text_thickness: 1.0,
209            num_steps: 0,
210            descending: false,
211            colors: [
212                Rgba8::new(255, 230, 204, 255), // 0: background (1.0, 0.9, 0.8)
213                Rgba8::new(179, 153, 153, 255), // 1: triangle (0.7, 0.6, 0.6)
214                Rgba8::new(0, 0, 0, 255),       // 2: text (black)
215                Rgba8::new(153, 102, 102, 102),  // 3: preview pointer (0.6,0.4,0.4, 0.4)
216                Rgba8::new(204, 0, 0, 153),      // 4: pointer (0.8, 0, 0, 0.6)
217                Rgba8::new(0, 0, 0, 255),       // 5: step markers (black)
218            ],
219            vertices: Vec::new(),
220            vertex_idx: 0,
221        };
222        s.calc_box();
223        s
224    }
225
226    fn calc_box(&mut self) {
227        self.xs1 = self.x1 + self.border_width;
228        self.ys1 = self.y1 + self.border_width;
229        self.xs2 = self.x2 - self.border_width;
230        self.ys2 = self.y2 - self.border_width;
231    }
232
233    /// Set the label format string. Use `%3.2f` as placeholder for the value.
234    pub fn label(&mut self, fmt: &str) {
235        self.label = fmt.to_string();
236    }
237
238    /// Set the value range.
239    pub fn range(&mut self, min: f64, max: f64) {
240        self.min = min;
241        self.max = max;
242    }
243
244    /// Get the current value (in user range).
245    pub fn value(&self) -> f64 {
246        self.value * (self.max - self.min) + self.min
247    }
248
249    /// Set the current value (in user range).
250    pub fn set_value(&mut self, v: f64) {
251        self.preview_value = ((v - self.min) / (self.max - self.min)).clamp(0.0, 1.0);
252        self.normalize_value(true);
253    }
254
255    /// Set the number of discrete steps (0 = continuous).
256    pub fn num_steps(&mut self, n: u32) {
257        self.num_steps = n;
258    }
259
260    pub fn set_descending(&mut self, d: bool) {
261        self.descending = d;
262    }
263
264    pub fn border_width(&mut self, t: f64, extra: f64) {
265        self.border_width = t;
266        self.border_extra = extra;
267        self.calc_box();
268    }
269
270    pub fn text_thickness(&mut self, t: f64) {
271        self.text_thickness = t;
272    }
273
274    fn normalize_value(&mut self, preview_value_flag: bool) {
275        if self.num_steps > 0 {
276            let step = (self.preview_value * self.num_steps as f64 + 0.5) as i32;
277            self.value = step as f64 / self.num_steps as f64;
278        } else {
279            self.value = self.preview_value;
280        }
281        if preview_value_flag {
282            self.preview_value = self.value;
283        }
284    }
285
286    /// Build vertices for the background rectangle (path 0).
287    fn calc_background(&mut self) {
288        let (x1, y1, x2, y2) = (self.x1, self.y1, self.x2, self.y2);
289        let be = self.border_extra;
290        self.vertices.clear();
291        self.vertices.push((x1 - be, y1 - be, PATH_CMD_MOVE_TO));
292        self.vertices.push((x2 + be, y1 - be, PATH_CMD_LINE_TO));
293        self.vertices.push((x2 + be, y2 + be, PATH_CMD_LINE_TO));
294        self.vertices.push((x1 - be, y2 + be, PATH_CMD_LINE_TO));
295    }
296
297    /// Build vertices for the triangle indicator (path 1).
298    fn calc_triangle(&mut self) {
299        self.vertices.clear();
300        if self.descending {
301            self.vertices.push((self.x1, self.y1, PATH_CMD_MOVE_TO));
302            self.vertices.push((self.x2, self.y1, PATH_CMD_LINE_TO));
303            self.vertices.push((self.x1, self.y2, PATH_CMD_LINE_TO));
304            self.vertices.push((self.x1, self.y1, PATH_CMD_LINE_TO));
305        } else {
306            self.vertices.push((self.x1, self.y1, PATH_CMD_MOVE_TO));
307            self.vertices.push((self.x2, self.y1, PATH_CMD_LINE_TO));
308            self.vertices.push((self.x2, self.y2, PATH_CMD_LINE_TO));
309            self.vertices.push((self.x1, self.y1, PATH_CMD_LINE_TO));
310        }
311    }
312
313    /// Build vertices for the label text (path 2).
314    fn calc_text(&mut self) {
315        self.vertices.clear();
316
317        let text = if self.label.contains('%') {
318            // Format the label with the value using C-style sprintf emulation.
319            // Parses %[width][.precision]f and %d patterns.
320            sprintf_format(&self.label, self.value())
321        } else if self.label.is_empty() {
322            return;
323        } else {
324            self.label.clone()
325        };
326
327        let text_height = self.y2 - self.y1;
328        let mut txt = GsvText::new();
329        txt.start_point(self.x1, self.y1);
330        txt.size(text_height * 1.2, text_height);
331        txt.text(&text);
332
333        let mut stroke = ConvStroke::new(txt);
334        stroke.set_width(self.text_thickness);
335        stroke.set_line_join(LineJoin::Round);
336        stroke.set_line_cap(LineCap::Round);
337
338        stroke.rewind(0);
339        loop {
340            let (mut x, mut y) = (0.0, 0.0);
341            let cmd = stroke.vertex(&mut x, &mut y);
342            if is_stop(cmd) {
343                break;
344            }
345            self.vertices.push((x, y, cmd));
346        }
347    }
348
349    /// Build vertices for the pointer preview circle (path 3).
350    fn calc_pointer_preview(&mut self) {
351        self.vertices.clear();
352        let cx = self.xs1 + (self.xs2 - self.xs1) * self.preview_value;
353        let cy = (self.ys1 + self.ys2) / 2.0;
354        let r = self.y2 - self.y1;
355
356        let mut ell = Ellipse::new(cx, cy, r, r, 32, false);
357        ell.rewind(0);
358        loop {
359            let (mut x, mut y) = (0.0, 0.0);
360            let cmd = ell.vertex(&mut x, &mut y);
361            if is_stop(cmd) {
362                break;
363            }
364            self.vertices.push((x, y, cmd));
365        }
366    }
367
368    /// Build vertices for the active pointer circle (path 4).
369    fn calc_pointer(&mut self) {
370        self.normalize_value(false);
371        self.vertices.clear();
372        let cx = self.xs1 + (self.xs2 - self.xs1) * self.value;
373        let cy = (self.ys1 + self.ys2) / 2.0;
374        let r = self.y2 - self.y1;
375
376        let mut ell = Ellipse::new(cx, cy, r, r, 32, false);
377        ell.rewind(0);
378        loop {
379            let (mut x, mut y) = (0.0, 0.0);
380            let cmd = ell.vertex(&mut x, &mut y);
381            if is_stop(cmd) {
382                break;
383            }
384            self.vertices.push((x, y, cmd));
385        }
386    }
387
388    /// Build vertices for step markers (path 5).
389    fn calc_steps(&mut self) {
390        self.vertices.clear();
391        if self.num_steps == 0 {
392            return;
393        }
394        let mut d = (self.xs2 - self.xs1) / self.num_steps as f64;
395        if d > 0.004 {
396            d = 0.004;
397        }
398        for i in 0..=self.num_steps {
399            let x = self.xs1 + (self.xs2 - self.xs1) * i as f64 / self.num_steps as f64;
400            self.vertices.push((x, self.y1, PATH_CMD_MOVE_TO));
401            self.vertices
402                .push((x - d * (self.x2 - self.x1), self.y1 - self.border_extra, PATH_CMD_LINE_TO));
403            self.vertices
404                .push((x + d * (self.x2 - self.x1), self.y1 - self.border_extra, PATH_CMD_LINE_TO));
405        }
406    }
407}
408
409impl Ctrl for SliderCtrl {
410    fn num_paths(&self) -> u32 {
411        6
412    }
413
414    fn color(&self, path_id: u32) -> Rgba8 {
415        self.colors[path_id.min(5) as usize]
416    }
417}
418
419impl VertexSource for SliderCtrl {
420    fn rewind(&mut self, path_id: u32) {
421        self.vertex_idx = 0;
422        match path_id {
423            0 => self.calc_background(),
424            1 => self.calc_triangle(),
425            2 => self.calc_text(),
426            3 => self.calc_pointer_preview(),
427            4 => self.calc_pointer(),
428            5 => self.calc_steps(),
429            _ => {
430                self.vertices.clear();
431            }
432        }
433    }
434
435    fn vertex(&mut self, x: &mut f64, y: &mut f64) -> u32 {
436        if self.vertex_idx < self.vertices.len() {
437            let (vx, vy, cmd) = self.vertices[self.vertex_idx];
438            *x = vx;
439            *y = vy;
440            self.vertex_idx += 1;
441            cmd
442        } else {
443            PATH_CMD_STOP
444        }
445    }
446}
447
448// ============================================================================
449// ScaleCtrl — dual-handle range slider
450// ============================================================================
451
452/// Two-handle range slider (min/max selector).
453///
454/// Renders 5 paths: background, border, selected range, left handle, right handle.
455pub struct ScaleCtrl {
456    x1: f64,
457    y1: f64,
458    x2: f64,
459    y2: f64,
460    xs1: f64,
461    ys1: f64,
462    xs2: f64,
463    ys2: f64,
464    border_width: f64,
465    border_extra: f64,
466    value1: f64,
467    value2: f64,
468    min_delta: f64,
469    colors: [Rgba8; 5],
470    vertices: Vec<(f64, f64, u32)>,
471    vertex_idx: usize,
472}
473
474impl ScaleCtrl {
475    pub fn new(x1: f64, y1: f64, x2: f64, y2: f64) -> Self {
476        let mut s = Self {
477            x1,
478            y1,
479            x2,
480            y2,
481            xs1: 0.0,
482            ys1: 0.0,
483            xs2: 0.0,
484            ys2: 0.0,
485            border_width: 1.0,
486            border_extra: (y2 - y1) * 0.5,
487            value1: 0.3,
488            value2: 0.7,
489            min_delta: 0.01,
490            colors: [
491                Rgba8::new(255, 230, 204, 255), // background
492                Rgba8::new(0, 0, 0, 255),       // border
493                Rgba8::new(180, 120, 120, 180), // selected range
494                Rgba8::new(204, 0, 0, 180),     // left handle
495                Rgba8::new(204, 0, 0, 180),     // right handle
496            ],
497            vertices: Vec::new(),
498            vertex_idx: 0,
499        };
500        s.calc_box();
501        s
502    }
503
504    fn calc_box(&mut self) {
505        self.xs1 = self.x1 + self.border_width;
506        self.ys1 = self.y1 + self.border_width;
507        self.xs2 = self.x2 - self.border_width;
508        self.ys2 = self.y2 - self.border_width;
509    }
510
511    pub fn set_min_delta(&mut self, d: f64) {
512        self.min_delta = d.clamp(0.0, 1.0);
513        self.set_value1(self.value1);
514        self.set_value2(self.value2);
515    }
516
517    pub fn value1(&self) -> f64 {
518        self.value1
519    }
520
521    pub fn value2(&self) -> f64 {
522        self.value2
523    }
524
525    pub fn set_value1(&mut self, v: f64) {
526        let max_v1 = (self.value2 - self.min_delta).clamp(0.0, 1.0);
527        self.value1 = v.clamp(0.0, max_v1);
528    }
529
530    pub fn set_value2(&mut self, v: f64) {
531        let min_v2 = (self.value1 + self.min_delta).clamp(0.0, 1.0);
532        self.value2 = v.clamp(min_v2, 1.0);
533    }
534
535    fn calc_background(&mut self) {
536        self.vertices.clear();
537        let be = self.border_extra;
538        self.vertices
539            .push((self.x1 - be, self.y1 - be, PATH_CMD_MOVE_TO));
540        self.vertices
541            .push((self.x2 + be, self.y1 - be, PATH_CMD_LINE_TO));
542        self.vertices
543            .push((self.x2 + be, self.y2 + be, PATH_CMD_LINE_TO));
544        self.vertices
545            .push((self.x1 - be, self.y2 + be, PATH_CMD_LINE_TO));
546    }
547
548    fn calc_border(&mut self) {
549        self.vertices.clear();
550        self.vertices.push((self.x1, self.y1, PATH_CMD_MOVE_TO));
551        self.vertices.push((self.x2, self.y1, PATH_CMD_LINE_TO));
552        self.vertices.push((self.x2, self.y2, PATH_CMD_LINE_TO));
553        self.vertices.push((self.x1, self.y2, PATH_CMD_LINE_TO));
554        self.vertices.push((
555            self.x1 + self.border_width,
556            self.y1 + self.border_width,
557            PATH_CMD_MOVE_TO,
558        ));
559        self.vertices.push((
560            self.x1 + self.border_width,
561            self.y2 - self.border_width,
562            PATH_CMD_LINE_TO,
563        ));
564        self.vertices.push((
565            self.x2 - self.border_width,
566            self.y2 - self.border_width,
567            PATH_CMD_LINE_TO,
568        ));
569        self.vertices.push((
570            self.x2 - self.border_width,
571            self.y1 + self.border_width,
572            PATH_CMD_LINE_TO,
573        ));
574    }
575
576    fn calc_selected_range(&mut self) {
577        self.vertices.clear();
578        let x1 = self.xs1 + (self.xs2 - self.xs1) * self.value1;
579        let x2 = self.xs1 + (self.xs2 - self.xs1) * self.value2;
580        self.vertices.push((x1, self.ys1, PATH_CMD_MOVE_TO));
581        self.vertices.push((x2, self.ys1, PATH_CMD_LINE_TO));
582        self.vertices.push((x2, self.ys2, PATH_CMD_LINE_TO));
583        self.vertices.push((x1, self.ys2, PATH_CMD_LINE_TO));
584    }
585
586    fn calc_handle(&mut self, v: f64) {
587        self.vertices.clear();
588        let cx = self.xs1 + (self.xs2 - self.xs1) * v;
589        let cy = (self.ys1 + self.ys2) * 0.5;
590        let r = (self.y2 - self.y1).max(1.0);
591        let mut ell = Ellipse::new(cx, cy, r, r, 32, false);
592        ell.rewind(0);
593        loop {
594            let (mut x, mut y) = (0.0, 0.0);
595            let cmd = ell.vertex(&mut x, &mut y);
596            if is_stop(cmd) {
597                break;
598            }
599            self.vertices.push((x, y, cmd));
600        }
601    }
602}
603
604impl Ctrl for ScaleCtrl {
605    fn num_paths(&self) -> u32 {
606        5
607    }
608
609    fn color(&self, path_id: u32) -> Rgba8 {
610        self.colors[path_id.min(4) as usize]
611    }
612}
613
614impl VertexSource for ScaleCtrl {
615    fn rewind(&mut self, path_id: u32) {
616        self.vertex_idx = 0;
617        match path_id {
618            0 => self.calc_background(),
619            1 => self.calc_border(),
620            2 => self.calc_selected_range(),
621            3 => self.calc_handle(self.value1),
622            4 => self.calc_handle(self.value2),
623            _ => self.vertices.clear(),
624        }
625    }
626
627    fn vertex(&mut self, x: &mut f64, y: &mut f64) -> u32 {
628        if self.vertex_idx < self.vertices.len() {
629            let (vx, vy, cmd) = self.vertices[self.vertex_idx];
630            *x = vx;
631            *y = vy;
632            self.vertex_idx += 1;
633            cmd
634        } else {
635            PATH_CMD_STOP
636        }
637    }
638}
639
640// ============================================================================
641// CboxCtrl — checkbox
642// ============================================================================
643
644/// Checkbox control rendered via AGG.
645///
646/// Renders 3 paths: box border (hollow), label text, checkmark (when active).
647///
648/// Port of C++ `cbox_ctrl_impl`.
649pub struct CboxCtrl {
650    x1: f64,
651    y1: f64,
652    x2: f64,
653    y2: f64,
654    text_thickness: f64,
655    text_height: f64,
656    text_width: f64,
657    label: String,
658    status: bool,
659    colors: [Rgba8; 3],
660    vertices: Vec<(f64, f64, u32)>,
661    vertex_idx: usize,
662}
663
664impl CboxCtrl {
665    pub fn new(x: f64, y: f64, label: &str) -> Self {
666        let text_height = 9.0;
667        Self {
668            x1: x,
669            y1: y,
670            x2: x + text_height * 1.5,
671            y2: y + text_height * 1.5,
672            text_thickness: 1.5,
673            text_height,
674            text_width: 0.0,
675            label: label.to_string(),
676            status: false,
677            colors: [
678                Rgba8::new(0, 0, 0, 255),     // 0: border (black)
679                Rgba8::new(0, 0, 0, 255),     // 1: text (black)
680                Rgba8::new(102, 0, 0, 255),   // 2: checkmark (dark red, 0.4,0,0)
681            ],
682            vertices: Vec::new(),
683            vertex_idx: 0,
684        }
685    }
686
687    pub fn set_status(&mut self, s: bool) {
688        self.status = s;
689    }
690
691    pub fn status(&self) -> bool {
692        self.status
693    }
694
695    pub fn text_size(&mut self, h: f64, w: f64) {
696        self.text_height = h;
697        self.text_width = w;
698    }
699
700    /// Build border vertices (path 0): outer rect + inner rect (hollow).
701    fn calc_border(&mut self) {
702        self.vertices.clear();
703        let t = self.text_thickness;
704        // Outer rectangle
705        self.vertices.push((self.x1, self.y1, PATH_CMD_MOVE_TO));
706        self.vertices.push((self.x2, self.y1, PATH_CMD_LINE_TO));
707        self.vertices.push((self.x2, self.y2, PATH_CMD_LINE_TO));
708        self.vertices.push((self.x1, self.y2, PATH_CMD_LINE_TO));
709        // Inner rectangle (winding creates hollow)
710        self.vertices.push((self.x1 + t, self.y1 + t, PATH_CMD_MOVE_TO));
711        self.vertices.push((self.x1 + t, self.y2 - t, PATH_CMD_LINE_TO));
712        self.vertices.push((self.x2 - t, self.y2 - t, PATH_CMD_LINE_TO));
713        self.vertices.push((self.x2 - t, self.y1 + t, PATH_CMD_LINE_TO));
714    }
715
716    /// Build text vertices (path 1).
717    fn calc_text(&mut self) {
718        self.vertices.clear();
719        let mut txt = GsvText::new();
720        txt.start_point(
721            self.x1 + self.text_height * 2.0,
722            self.y1 + self.text_height / 5.0,
723        );
724        txt.size(self.text_height, self.text_width);
725        txt.text(&self.label);
726
727        let mut stroke = ConvStroke::new(txt);
728        stroke.set_width(self.text_thickness);
729        stroke.set_line_join(LineJoin::Round);
730        stroke.set_line_cap(LineCap::Round);
731
732        stroke.rewind(0);
733        loop {
734            let (mut x, mut y) = (0.0, 0.0);
735            let cmd = stroke.vertex(&mut x, &mut y);
736            if is_stop(cmd) {
737                break;
738            }
739            self.vertices.push((x, y, cmd));
740        }
741    }
742
743    /// Build checkmark vertices (path 2): octagon star, only when active.
744    fn calc_checkmark(&mut self) {
745        self.vertices.clear();
746        if !self.status {
747            return;
748        }
749        let d2 = (self.y2 - self.y1) / 2.0;
750        let t = self.text_thickness * 1.5;
751        // 8-vertex star pattern matching C++ cbox_ctrl exactly
752        self.vertices.push((self.x1 + self.text_thickness, self.y1 + self.text_thickness, PATH_CMD_MOVE_TO));
753        self.vertices.push((self.x1 + d2, self.y1 + d2 - t, PATH_CMD_LINE_TO));
754        self.vertices.push((self.x2 - self.text_thickness, self.y1 + self.text_thickness, PATH_CMD_LINE_TO));
755        self.vertices.push((self.x1 + d2 + t, self.y1 + d2, PATH_CMD_LINE_TO));
756        self.vertices.push((self.x2 - self.text_thickness, self.y2 - self.text_thickness, PATH_CMD_LINE_TO));
757        self.vertices.push((self.x1 + d2, self.y1 + d2 + t, PATH_CMD_LINE_TO));
758        self.vertices.push((self.x1 + self.text_thickness, self.y2 - self.text_thickness, PATH_CMD_LINE_TO));
759        self.vertices.push((self.x1 + d2 - t, self.y1 + d2, PATH_CMD_LINE_TO));
760    }
761}
762
763impl Ctrl for CboxCtrl {
764    fn num_paths(&self) -> u32 {
765        3
766    }
767
768    fn color(&self, path_id: u32) -> Rgba8 {
769        self.colors[path_id.min(2) as usize]
770    }
771}
772
773impl VertexSource for CboxCtrl {
774    fn rewind(&mut self, path_id: u32) {
775        self.vertex_idx = 0;
776        match path_id {
777            0 => self.calc_border(),
778            1 => self.calc_text(),
779            2 => self.calc_checkmark(),
780            _ => {
781                self.vertices.clear();
782            }
783        }
784    }
785
786    fn vertex(&mut self, x: &mut f64, y: &mut f64) -> u32 {
787        if self.vertex_idx < self.vertices.len() {
788            let (vx, vy, cmd) = self.vertices[self.vertex_idx];
789            *x = vx;
790            *y = vy;
791            self.vertex_idx += 1;
792            cmd
793        } else {
794            PATH_CMD_STOP
795        }
796    }
797}
798
799// ============================================================================
800// RboxCtrl — radio button box
801// ============================================================================
802
803/// Radio button box control rendered via AGG.
804///
805/// Renders 5 paths: background, border (hollow), item text labels,
806/// inactive radio circles (stroked), active radio circle (filled).
807///
808/// Port of C++ `rbox_ctrl_impl`.
809pub struct RboxCtrl {
810    x1: f64,
811    y1: f64,
812    x2: f64,
813    y2: f64,
814    xs1: f64,
815    ys1: f64,
816    xs2: f64,
817    ys2: f64,
818    border_width: f64,
819    border_extra: f64,
820    text_thickness: f64,
821    text_height: f64,
822    text_width: f64,
823    items: Vec<String>,
824    cur_item: i32,
825    dy: f64,
826    colors: [Rgba8; 5],
827    vertices: Vec<(f64, f64, u32)>,
828    vertex_idx: usize,
829}
830
831impl RboxCtrl {
832    pub fn new(x1: f64, y1: f64, x2: f64, y2: f64) -> Self {
833        let mut r = Self {
834            x1,
835            y1,
836            x2,
837            y2,
838            xs1: 0.0,
839            ys1: 0.0,
840            xs2: 0.0,
841            ys2: 0.0,
842            border_width: 1.0,
843            border_extra: 0.0,
844            text_thickness: 1.5,
845            text_height: 9.0,
846            text_width: 0.0,
847            items: Vec::new(),
848            cur_item: -1,
849            dy: 18.0,
850            colors: [
851                Rgba8::new(255, 255, 230, 255), // 0: background (1.0, 1.0, 0.9)
852                Rgba8::new(0, 0, 0, 255),       // 1: border (black)
853                Rgba8::new(0, 0, 0, 255),       // 2: text (black)
854                Rgba8::new(0, 0, 0, 255),       // 3: inactive circles (black)
855                Rgba8::new(102, 0, 0, 255),     // 4: active circle (dark red, 0.4,0,0)
856            ],
857            vertices: Vec::new(),
858            vertex_idx: 0,
859        };
860        r.calc_rbox();
861        r
862    }
863
864    fn calc_rbox(&mut self) {
865        self.xs1 = self.x1 + self.border_width;
866        self.ys1 = self.y1 + self.border_width;
867        self.xs2 = self.x2 - self.border_width;
868        self.ys2 = self.y2 - self.border_width;
869    }
870
871    pub fn add_item(&mut self, text: &str) {
872        self.items.push(text.to_string());
873    }
874
875    pub fn cur_item(&self) -> i32 {
876        self.cur_item
877    }
878
879    pub fn set_cur_item(&mut self, i: i32) {
880        self.cur_item = i;
881    }
882
883    pub fn text_size(&mut self, h: f64, w: f64) {
884        self.text_height = h;
885        self.text_width = w;
886    }
887
888    pub fn border_width(&mut self, t: f64, extra: f64) {
889        self.border_width = t;
890        self.border_extra = extra;
891        self.calc_rbox();
892    }
893
894    pub fn text_thickness(&mut self, t: f64) {
895        self.text_thickness = t;
896    }
897
898    pub fn background_color(&mut self, c: Rgba8) {
899        self.colors[0] = c;
900    }
901
902    /// Build background rectangle (path 0).
903    fn calc_background(&mut self) {
904        self.vertices.clear();
905        let be = self.border_extra;
906        self.vertices.push((self.x1 - be, self.y1 - be, PATH_CMD_MOVE_TO));
907        self.vertices.push((self.x2 + be, self.y1 - be, PATH_CMD_LINE_TO));
908        self.vertices.push((self.x2 + be, self.y2 + be, PATH_CMD_LINE_TO));
909        self.vertices.push((self.x1 - be, self.y2 + be, PATH_CMD_LINE_TO));
910    }
911
912    /// Build border (path 1): outer rect + inner rect (hollow).
913    fn calc_border(&mut self) {
914        self.vertices.clear();
915        let bw = self.border_width;
916        // Outer
917        self.vertices.push((self.x1, self.y1, PATH_CMD_MOVE_TO));
918        self.vertices.push((self.x2, self.y1, PATH_CMD_LINE_TO));
919        self.vertices.push((self.x2, self.y2, PATH_CMD_LINE_TO));
920        self.vertices.push((self.x1, self.y2, PATH_CMD_LINE_TO));
921        // Inner
922        self.vertices.push((self.x1 + bw, self.y1 + bw, PATH_CMD_MOVE_TO));
923        self.vertices.push((self.x1 + bw, self.y2 - bw, PATH_CMD_LINE_TO));
924        self.vertices.push((self.x2 - bw, self.y2 - bw, PATH_CMD_LINE_TO));
925        self.vertices.push((self.x2 - bw, self.y1 + bw, PATH_CMD_LINE_TO));
926    }
927
928    /// Build all item text labels (path 2).
929    fn calc_text(&mut self) {
930        self.vertices.clear();
931        let dy = self.text_height * 2.0;
932
933        for (i, item) in self.items.iter().enumerate() {
934            let mut txt = GsvText::new();
935            txt.start_point(self.xs1 + dy * 1.5, self.ys1 + dy * (i as f64 + 1.0) - dy / 2.0);
936            txt.size(self.text_height, self.text_width);
937            txt.text(item);
938
939            let mut stroke = ConvStroke::new(txt);
940            stroke.set_width(self.text_thickness);
941            stroke.set_line_join(LineJoin::Round);
942            stroke.set_line_cap(LineCap::Round);
943
944            stroke.rewind(0);
945            loop {
946                let (mut x, mut y) = (0.0, 0.0);
947                let cmd = stroke.vertex(&mut x, &mut y);
948                if is_stop(cmd) {
949                    break;
950                }
951                self.vertices.push((x, y, cmd));
952            }
953        }
954    }
955
956    /// Build inactive radio circles (path 3): stroked circle outlines.
957    fn calc_inactive_circles(&mut self) {
958        self.vertices.clear();
959        let dy = self.text_height * 2.0;
960        let r = self.text_height / 1.5;
961
962        for i in 0..self.items.len() {
963            let cx = self.xs1 + dy / 1.3;
964            let cy = self.ys1 + dy * i as f64 + dy / 1.3;
965
966            let mut ell = Ellipse::new(cx, cy, r, r, 32, false);
967            let mut stroke = ConvStroke::new(&mut ell);
968            stroke.set_width(self.text_thickness);
969
970            stroke.rewind(0);
971            loop {
972                let (mut x, mut y) = (0.0, 0.0);
973                let cmd = stroke.vertex(&mut x, &mut y);
974                if is_stop(cmd) {
975                    break;
976                }
977                self.vertices.push((x, y, cmd));
978            }
979        }
980    }
981
982    /// Build the active radio circle (path 4): filled smaller circle.
983    fn calc_active_circle(&mut self) {
984        self.vertices.clear();
985        if self.cur_item < 0 {
986            return;
987        }
988        let dy = self.text_height * 2.0;
989        let cx = self.xs1 + dy / 1.3;
990        let cy = self.ys1 + dy * self.cur_item as f64 + dy / 1.3;
991        let r = self.text_height / 2.0;
992
993        let mut ell = Ellipse::new(cx, cy, r, r, 32, false);
994        ell.rewind(0);
995        loop {
996            let (mut x, mut y) = (0.0, 0.0);
997            let cmd = ell.vertex(&mut x, &mut y);
998            if is_stop(cmd) {
999                break;
1000            }
1001            self.vertices.push((x, y, cmd));
1002        }
1003    }
1004}
1005
1006impl Ctrl for RboxCtrl {
1007    fn num_paths(&self) -> u32 {
1008        5
1009    }
1010
1011    fn color(&self, path_id: u32) -> Rgba8 {
1012        self.colors[path_id.min(4) as usize]
1013    }
1014}
1015
1016impl VertexSource for RboxCtrl {
1017    fn rewind(&mut self, path_id: u32) {
1018        self.vertex_idx = 0;
1019        self.dy = self.text_height * 2.0;
1020        match path_id {
1021            0 => self.calc_background(),
1022            1 => self.calc_border(),
1023            2 => self.calc_text(),
1024            3 => self.calc_inactive_circles(),
1025            4 => self.calc_active_circle(),
1026            _ => {
1027                self.vertices.clear();
1028            }
1029        }
1030    }
1031
1032    fn vertex(&mut self, x: &mut f64, y: &mut f64) -> u32 {
1033        if self.vertex_idx < self.vertices.len() {
1034            let (vx, vy, cmd) = self.vertices[self.vertex_idx];
1035            *x = vx;
1036            *y = vy;
1037            self.vertex_idx += 1;
1038            cmd
1039        } else {
1040            PATH_CMD_STOP
1041        }
1042    }
1043}
1044
1045// ============================================================================
1046// SplineCtrl — spline editor/control
1047// ============================================================================
1048
1049/// Spline control rendered via AGG.
1050///
1051/// Renders 5 paths: background, border, curve, inactive points, active point.
1052///
1053/// Port of C++ `spline_ctrl_impl` / `spline_ctrl<ColorT>`.
1054pub struct SplineCtrl {
1055    x1: f64,
1056    y1: f64,
1057    x2: f64,
1058    y2: f64,
1059    num_pnt: usize,
1060    xp: [f64; 32],
1061    yp: [f64; 32],
1062    spline_values: [f64; 256],
1063    spline_values8: [u8; 256],
1064    border_width: f64,
1065    border_extra: f64,
1066    curve_width: f64,
1067    point_size: f64,
1068    xs1: f64,
1069    ys1: f64,
1070    xs2: f64,
1071    ys2: f64,
1072    active_pnt: i32,
1073    colors: [Rgba8; 5],
1074    vertices: Vec<(f64, f64, u32)>,
1075    vertex_idx: usize,
1076}
1077
1078impl SplineCtrl {
1079    pub fn new(x1: f64, y1: f64, x2: f64, y2: f64, num_pnt: usize) -> Self {
1080        let n = num_pnt.clamp(4, 32);
1081        let mut s = Self {
1082            x1,
1083            y1,
1084            x2,
1085            y2,
1086            num_pnt: n,
1087            xp: [0.0; 32],
1088            yp: [0.0; 32],
1089            spline_values: [0.0; 256],
1090            spline_values8: [0; 256],
1091            border_width: 1.0,
1092            border_extra: 0.0,
1093            curve_width: 1.0,
1094            point_size: 3.0,
1095            xs1: 0.0,
1096            ys1: 0.0,
1097            xs2: 0.0,
1098            ys2: 0.0,
1099            active_pnt: -1,
1100            colors: [
1101                Rgba8::new(255, 255, 230, 255), // background
1102                Rgba8::new(0, 0, 0, 255),       // border
1103                Rgba8::new(0, 0, 0, 255),       // curve
1104                Rgba8::new(0, 0, 0, 255),       // inactive points
1105                Rgba8::new(255, 0, 0, 255),     // active point
1106            ],
1107            vertices: Vec::new(),
1108            vertex_idx: 0,
1109        };
1110        for i in 0..s.num_pnt {
1111            s.xp[i] = i as f64 / (s.num_pnt - 1) as f64;
1112            s.yp[i] = 0.5;
1113        }
1114        s.calc_spline_box();
1115        s.update_spline();
1116        s
1117    }
1118
1119    pub fn border_width(&mut self, t: f64, extra: f64) {
1120        self.border_width = t;
1121        self.border_extra = extra;
1122        self.calc_spline_box();
1123    }
1124
1125    pub fn curve_width(&mut self, t: f64) {
1126        self.curve_width = t;
1127    }
1128
1129    pub fn point_size(&mut self, s: f64) {
1130        self.point_size = s;
1131    }
1132
1133    pub fn active_point(&mut self, i: i32) {
1134        self.active_pnt = i;
1135    }
1136
1137    pub fn background_color(&mut self, c: Rgba8) {
1138        self.colors[0] = c;
1139    }
1140
1141    pub fn border_color(&mut self, c: Rgba8) {
1142        self.colors[1] = c;
1143    }
1144
1145    pub fn curve_color(&mut self, c: Rgba8) {
1146        self.colors[2] = c;
1147    }
1148
1149    pub fn inactive_pnt_color(&mut self, c: Rgba8) {
1150        self.colors[3] = c;
1151    }
1152
1153    pub fn active_pnt_color(&mut self, c: Rgba8) {
1154        self.colors[4] = c;
1155    }
1156
1157    fn calc_spline_box(&mut self) {
1158        self.xs1 = self.x1 + self.border_width;
1159        self.ys1 = self.y1 + self.border_width;
1160        self.xs2 = self.x2 - self.border_width;
1161        self.ys2 = self.y2 - self.border_width;
1162    }
1163
1164    fn set_xp(&mut self, idx: usize, mut val: f64) {
1165        val = val.clamp(0.0, 1.0);
1166        if idx == 0 {
1167            val = 0.0;
1168        } else if idx == self.num_pnt - 1 {
1169            val = 1.0;
1170        } else {
1171            if val < self.xp[idx - 1] + 0.001 {
1172                val = self.xp[idx - 1] + 0.001;
1173            }
1174            if val > self.xp[idx + 1] - 0.001 {
1175                val = self.xp[idx + 1] - 0.001;
1176            }
1177        }
1178        self.xp[idx] = val;
1179    }
1180
1181    fn set_yp(&mut self, idx: usize, val: f64) {
1182        self.yp[idx] = val.clamp(0.0, 1.0);
1183    }
1184
1185    pub fn point(&mut self, idx: usize, x: f64, y: f64) {
1186        if idx < self.num_pnt {
1187            self.set_xp(idx, x);
1188            self.set_yp(idx, y);
1189        }
1190    }
1191
1192    pub fn value(&self, x: f64) -> f64 {
1193        let mut s = Bspline::new();
1194        s.init(&self.xp[..self.num_pnt], &self.yp[..self.num_pnt]);
1195        s.get(x).clamp(0.0, 1.0)
1196    }
1197
1198    pub fn value_at_point(&mut self, idx: usize, y: f64) {
1199        if idx < self.num_pnt {
1200            self.set_yp(idx, y);
1201        }
1202    }
1203
1204    pub fn x(&self, idx: usize) -> f64 {
1205        self.xp[idx]
1206    }
1207
1208    pub fn y(&self, idx: usize) -> f64 {
1209        self.yp[idx]
1210    }
1211
1212    pub fn update_spline(&mut self) {
1213        let mut spline = Bspline::new();
1214        spline.init(&self.xp[..self.num_pnt], &self.yp[..self.num_pnt]);
1215        for i in 0..256 {
1216            let v = spline.get(i as f64 / 255.0).clamp(0.0, 1.0);
1217            self.spline_values[i] = v;
1218            self.spline_values8[i] = (v * 255.0) as u8;
1219        }
1220    }
1221
1222    pub fn spline(&self) -> &[f64; 256] {
1223        &self.spline_values
1224    }
1225
1226    pub fn spline8(&self) -> &[u8; 256] {
1227        &self.spline_values8
1228    }
1229
1230    fn calc_xp(&self, idx: usize) -> f64 {
1231        self.xs1 + (self.xs2 - self.xs1) * self.xp[idx]
1232    }
1233
1234    fn calc_yp(&self, idx: usize) -> f64 {
1235        self.ys1 + (self.ys2 - self.ys1) * self.yp[idx]
1236    }
1237
1238    fn calc_background(&mut self) {
1239        self.vertices.clear();
1240        let be = self.border_extra;
1241        self.vertices
1242            .push((self.x1 - be, self.y1 - be, PATH_CMD_MOVE_TO));
1243        self.vertices
1244            .push((self.x2 + be, self.y1 - be, PATH_CMD_LINE_TO));
1245        self.vertices
1246            .push((self.x2 + be, self.y2 + be, PATH_CMD_LINE_TO));
1247        self.vertices
1248            .push((self.x1 - be, self.y2 + be, PATH_CMD_LINE_TO));
1249    }
1250
1251    fn calc_border(&mut self) {
1252        self.vertices.clear();
1253        self.vertices.push((self.x1, self.y1, PATH_CMD_MOVE_TO));
1254        self.vertices.push((self.x2, self.y1, PATH_CMD_LINE_TO));
1255        self.vertices.push((self.x2, self.y2, PATH_CMD_LINE_TO));
1256        self.vertices.push((self.x1, self.y2, PATH_CMD_LINE_TO));
1257        self.vertices.push((
1258            self.x1 + self.border_width,
1259            self.y1 + self.border_width,
1260            PATH_CMD_MOVE_TO,
1261        ));
1262        self.vertices.push((
1263            self.x1 + self.border_width,
1264            self.y2 - self.border_width,
1265            PATH_CMD_LINE_TO,
1266        ));
1267        self.vertices.push((
1268            self.x2 - self.border_width,
1269            self.y2 - self.border_width,
1270            PATH_CMD_LINE_TO,
1271        ));
1272        self.vertices.push((
1273            self.x2 - self.border_width,
1274            self.y1 + self.border_width,
1275            PATH_CMD_LINE_TO,
1276        ));
1277    }
1278
1279    fn calc_curve(&mut self) {
1280        self.vertices.clear();
1281        let mut path = PathStorage::new();
1282        path.move_to(self.xs1, self.ys1 + (self.ys2 - self.ys1) * self.spline_values[0]);
1283        for i in 1..256 {
1284            path.line_to(
1285                self.xs1 + (self.xs2 - self.xs1) * i as f64 / 255.0,
1286                self.ys1 + (self.ys2 - self.ys1) * self.spline_values[i],
1287            );
1288        }
1289        let mut stroke = ConvStroke::new(&mut path);
1290        stroke.set_width(self.curve_width);
1291        stroke.rewind(0);
1292        loop {
1293            let (mut x, mut y) = (0.0, 0.0);
1294            let cmd = stroke.vertex(&mut x, &mut y);
1295            if is_stop(cmd) {
1296                break;
1297            }
1298            self.vertices.push((x, y, cmd));
1299        }
1300    }
1301
1302    fn calc_inactive_points(&mut self) {
1303        self.vertices.clear();
1304        for i in 0..self.num_pnt {
1305            if i as i32 == self.active_pnt {
1306                continue;
1307            }
1308            let mut ell = Ellipse::new(
1309                self.calc_xp(i),
1310                self.calc_yp(i),
1311                self.point_size,
1312                self.point_size,
1313                32,
1314                false,
1315            );
1316            ell.rewind(0);
1317            loop {
1318                let (mut x, mut y) = (0.0, 0.0);
1319                let cmd = ell.vertex(&mut x, &mut y);
1320                if is_stop(cmd) {
1321                    break;
1322                }
1323                self.vertices.push((x, y, cmd));
1324            }
1325        }
1326    }
1327
1328    fn calc_active_point(&mut self) {
1329        self.vertices.clear();
1330        if self.active_pnt < 0 || self.active_pnt as usize >= self.num_pnt {
1331            return;
1332        }
1333        let i = self.active_pnt as usize;
1334        let mut ell = Ellipse::new(
1335            self.calc_xp(i),
1336            self.calc_yp(i),
1337            self.point_size,
1338            self.point_size,
1339            32,
1340            false,
1341        );
1342        ell.rewind(0);
1343        loop {
1344            let (mut x, mut y) = (0.0, 0.0);
1345            let cmd = ell.vertex(&mut x, &mut y);
1346            if is_stop(cmd) {
1347                break;
1348            }
1349            self.vertices.push((x, y, cmd));
1350        }
1351    }
1352}
1353
1354impl Ctrl for SplineCtrl {
1355    fn num_paths(&self) -> u32 {
1356        5
1357    }
1358
1359    fn color(&self, path_id: u32) -> Rgba8 {
1360        self.colors[path_id.min(4) as usize]
1361    }
1362}
1363
1364impl VertexSource for SplineCtrl {
1365    fn rewind(&mut self, path_id: u32) {
1366        self.vertex_idx = 0;
1367        match path_id {
1368            0 => self.calc_background(),
1369            1 => self.calc_border(),
1370            2 => self.calc_curve(),
1371            3 => self.calc_inactive_points(),
1372            4 => self.calc_active_point(),
1373            _ => self.vertices.clear(),
1374        }
1375    }
1376
1377    fn vertex(&mut self, x: &mut f64, y: &mut f64) -> u32 {
1378        if self.vertex_idx < self.vertices.len() {
1379            let (vx, vy, cmd) = self.vertices[self.vertex_idx];
1380            *x = vx;
1381            *y = vy;
1382            self.vertex_idx += 1;
1383            cmd
1384        } else {
1385            PATH_CMD_STOP
1386        }
1387    }
1388}
1389
1390// ============================================================================
1391// GammaCtrl — interactive gamma spline control
1392// ============================================================================
1393
1394/// Interactive gamma correction curve control.
1395///
1396/// Renders 7 paths: background, border, gamma curve, grid/crosshairs,
1397/// inactive point, active point, and text display.
1398///
1399/// Port of C++ `gamma_ctrl_impl` + `gamma_ctrl<ColorT>`.
1400pub struct GammaCtrl {
1401    // Widget bounds
1402    x1: f64,
1403    y1: f64,
1404    x2: f64,
1405    y2: f64,
1406    // Gamma spline
1407    gamma_spline: crate::gamma::GammaSpline,
1408    // Visual parameters
1409    border_width: f64,
1410    border_extra: f64,
1411    curve_width: f64,
1412    grid_width: f64,
1413    text_thickness: f64,
1414    point_size: f64,
1415    text_height: f64,
1416    text_width: f64,
1417    // Chart area
1418    xc1: f64,
1419    yc1: f64,
1420    xc2: f64,
1421    yc2: f64,
1422    // Spline drawing area
1423    xs1: f64,
1424    ys1: f64,
1425    xs2: f64,
1426    ys2: f64,
1427    // Text area
1428    xt1: f64,
1429    yt1: f64,
1430    #[allow(dead_code)]
1431    xt2: f64,
1432    #[allow(dead_code)]
1433    yt2: f64,
1434    // Control point positions
1435    xp1: f64,
1436    yp1: f64,
1437    xp2: f64,
1438    yp2: f64,
1439    // Interaction state
1440    p1_active: bool,
1441    mouse_point: u32,
1442    pdx: f64,
1443    pdy: f64,
1444    // Colors (7 paths)
1445    colors: [Rgba8; 7],
1446    // Rendering state
1447    vertices: Vec<(f64, f64, u32)>,
1448    vertex_idx: usize,
1449}
1450
1451impl GammaCtrl {
1452    pub fn new(x1: f64, y1: f64, x2: f64, y2: f64) -> Self {
1453        let text_height = 9.0;
1454        let border_width = 2.0;
1455        let yc2 = y2 - text_height * 2.0;
1456
1457        let mut gc = Self {
1458            x1,
1459            y1,
1460            x2,
1461            y2,
1462            gamma_spline: crate::gamma::GammaSpline::new(),
1463            border_width,
1464            border_extra: 0.0,
1465            curve_width: 2.0,
1466            grid_width: 0.2,
1467            text_thickness: 1.5,
1468            point_size: 5.0,
1469            text_height,
1470            text_width: 0.0,
1471            xc1: x1,
1472            yc1: y1,
1473            xc2: x2,
1474            yc2,
1475            xs1: 0.0,
1476            ys1: 0.0,
1477            xs2: 0.0,
1478            ys2: 0.0,
1479            xt1: x1,
1480            yt1: yc2,
1481            xt2: x2,
1482            yt2: y2,
1483            xp1: 0.0,
1484            yp1: 0.0,
1485            xp2: 0.0,
1486            yp2: 0.0,
1487            p1_active: true,
1488            mouse_point: 0,
1489            pdx: 0.0,
1490            pdy: 0.0,
1491            colors: [
1492                Rgba8::new(255, 255, 230, 255), // 0: background (1.0, 1.0, 0.9)
1493                Rgba8::new(0, 0, 0, 255),       // 1: border (black)
1494                Rgba8::new(0, 0, 0, 255),       // 2: curve (black)
1495                Rgba8::new(51, 51, 0, 255),     // 3: grid (0.2, 0.2, 0.0)
1496                Rgba8::new(0, 0, 0, 255),       // 4: inactive point (black)
1497                Rgba8::new(255, 0, 0, 255),     // 5: active point (red)
1498                Rgba8::new(0, 0, 0, 255),       // 6: text (black)
1499            ],
1500            vertices: Vec::new(),
1501            vertex_idx: 0,
1502        };
1503        gc.calc_spline_box();
1504        gc
1505    }
1506
1507    fn calc_spline_box(&mut self) {
1508        self.xs1 = self.xc1 + self.border_width;
1509        self.ys1 = self.yc1 + self.border_width;
1510        self.xs2 = self.xc2 - self.border_width;
1511        self.ys2 = self.yc2 - self.border_width * 0.5;
1512    }
1513
1514    fn calc_points(&mut self) {
1515        let (kx1, ky1, kx2, ky2) = self.gamma_spline.get_values();
1516        self.xp1 = self.xs1 + (self.xs2 - self.xs1) * kx1 * 0.25;
1517        self.yp1 = self.ys1 + (self.ys2 - self.ys1) * ky1 * 0.25;
1518        self.xp2 = self.xs2 - (self.xs2 - self.xs1) * kx2 * 0.25;
1519        self.yp2 = self.ys2 - (self.ys2 - self.ys1) * ky2 * 0.25;
1520    }
1521
1522    fn calc_values(&mut self) {
1523        let kx1 = (self.xp1 - self.xs1) * 4.0 / (self.xs2 - self.xs1);
1524        let ky1 = (self.yp1 - self.ys1) * 4.0 / (self.ys2 - self.ys1);
1525        let kx2 = (self.xs2 - self.xp2) * 4.0 / (self.xs2 - self.xs1);
1526        let ky2 = (self.ys2 - self.yp2) * 4.0 / (self.ys2 - self.ys1);
1527        self.gamma_spline.set_values(kx1, ky1, kx2, ky2);
1528    }
1529
1530    // --- Configuration ---
1531
1532    pub fn border_width(&mut self, t: f64, extra: f64) {
1533        self.border_width = t;
1534        self.border_extra = extra;
1535        self.calc_spline_box();
1536    }
1537
1538    pub fn curve_width(&mut self, t: f64) {
1539        self.curve_width = t;
1540    }
1541
1542    pub fn grid_width(&mut self, t: f64) {
1543        self.grid_width = t;
1544    }
1545
1546    pub fn text_thickness(&mut self, t: f64) {
1547        self.text_thickness = t;
1548    }
1549
1550    pub fn text_size(&mut self, h: f64, w: f64) {
1551        self.text_width = w;
1552        self.text_height = h;
1553        self.yc2 = self.y2 - self.text_height * 2.0;
1554        self.yt1 = self.y2 - self.text_height * 2.0;
1555        self.calc_spline_box();
1556    }
1557
1558    pub fn point_size(&mut self, s: f64) {
1559        self.point_size = s;
1560    }
1561
1562    // --- Spline value access ---
1563
1564    pub fn set_values(&mut self, kx1: f64, ky1: f64, kx2: f64, ky2: f64) {
1565        self.gamma_spline.set_values(kx1, ky1, kx2, ky2);
1566    }
1567
1568    pub fn get_values(&self) -> (f64, f64, f64, f64) {
1569        self.gamma_spline.get_values()
1570    }
1571
1572    pub fn gamma(&self) -> &[u8; 256] {
1573        self.gamma_spline.gamma()
1574    }
1575
1576    pub fn y(&self, x: f64) -> f64 {
1577        self.gamma_spline.y(x)
1578    }
1579
1580    pub fn get_gamma_spline(&self) -> &crate::gamma::GammaSpline {
1581        &self.gamma_spline
1582    }
1583
1584    pub fn change_active_point(&mut self) {
1585        self.p1_active = !self.p1_active;
1586    }
1587
1588    // --- Color setters ---
1589
1590    pub fn background_color(&mut self, c: Rgba8) {
1591        self.colors[0] = c;
1592    }
1593    pub fn border_color(&mut self, c: Rgba8) {
1594        self.colors[1] = c;
1595    }
1596    pub fn curve_color(&mut self, c: Rgba8) {
1597        self.colors[2] = c;
1598    }
1599    pub fn grid_color(&mut self, c: Rgba8) {
1600        self.colors[3] = c;
1601    }
1602    pub fn inactive_pnt_color(&mut self, c: Rgba8) {
1603        self.colors[4] = c;
1604    }
1605    pub fn active_pnt_color(&mut self, c: Rgba8) {
1606        self.colors[5] = c;
1607    }
1608    pub fn text_color(&mut self, c: Rgba8) {
1609        self.colors[6] = c;
1610    }
1611
1612    // --- Mouse interaction ---
1613
1614    pub fn in_rect(&self, x: f64, y: f64) -> bool {
1615        x >= self.x1 && x <= self.x2 && y >= self.y1 && y <= self.y2
1616    }
1617
1618    pub fn on_mouse_button_down(&mut self, x: f64, y: f64) -> bool {
1619        self.calc_points();
1620        let dist1 = ((x - self.xp1).powi(2) + (y - self.yp1).powi(2)).sqrt();
1621        if dist1 <= self.point_size + 1.0 {
1622            self.mouse_point = 1;
1623            self.pdx = self.xp1 - x;
1624            self.pdy = self.yp1 - y;
1625            self.p1_active = true;
1626            return true;
1627        }
1628        let dist2 = ((x - self.xp2).powi(2) + (y - self.yp2).powi(2)).sqrt();
1629        if dist2 <= self.point_size + 1.0 {
1630            self.mouse_point = 2;
1631            self.pdx = self.xp2 - x;
1632            self.pdy = self.yp2 - y;
1633            self.p1_active = false;
1634            return true;
1635        }
1636        false
1637    }
1638
1639    pub fn on_mouse_button_up(&mut self, _x: f64, _y: f64) -> bool {
1640        if self.mouse_point != 0 {
1641            self.mouse_point = 0;
1642            return true;
1643        }
1644        false
1645    }
1646
1647    pub fn on_mouse_move(&mut self, x: f64, y: f64, button_flag: bool) -> bool {
1648        if !button_flag {
1649            return self.on_mouse_button_up(x, y);
1650        }
1651        if self.mouse_point == 1 {
1652            self.xp1 = x + self.pdx;
1653            self.yp1 = y + self.pdy;
1654            self.calc_values();
1655            return true;
1656        }
1657        if self.mouse_point == 2 {
1658            self.xp2 = x + self.pdx;
1659            self.yp2 = y + self.pdy;
1660            self.calc_values();
1661            return true;
1662        }
1663        false
1664    }
1665
1666    pub fn on_arrow_keys(&mut self, left: bool, right: bool, down: bool, up: bool) -> bool {
1667        let (mut kx1, mut ky1, mut kx2, mut ky2) = self.gamma_spline.get_values();
1668        let mut ret = false;
1669        if self.p1_active {
1670            if left {
1671                kx1 -= 0.005;
1672                ret = true;
1673            }
1674            if right {
1675                kx1 += 0.005;
1676                ret = true;
1677            }
1678            if down {
1679                ky1 -= 0.005;
1680                ret = true;
1681            }
1682            if up {
1683                ky1 += 0.005;
1684                ret = true;
1685            }
1686        } else {
1687            if left {
1688                kx2 += 0.005;
1689                ret = true;
1690            }
1691            if right {
1692                kx2 -= 0.005;
1693                ret = true;
1694            }
1695            if down {
1696                ky2 += 0.005;
1697                ret = true;
1698            }
1699            if up {
1700                ky2 -= 0.005;
1701                ret = true;
1702            }
1703        }
1704        if ret {
1705            self.gamma_spline.set_values(kx1, ky1, kx2, ky2);
1706        }
1707        ret
1708    }
1709
1710    // --- Path generation helpers ---
1711
1712    /// Path 0: Background rectangle.
1713    fn calc_background(&mut self) {
1714        self.vertices.clear();
1715        let be = self.border_extra;
1716        self.vertices
1717            .push((self.x1 - be, self.y1 - be, PATH_CMD_MOVE_TO));
1718        self.vertices
1719            .push((self.x2 + be, self.y1 - be, PATH_CMD_LINE_TO));
1720        self.vertices
1721            .push((self.x2 + be, self.y2 + be, PATH_CMD_LINE_TO));
1722        self.vertices
1723            .push((self.x1 - be, self.y2 + be, PATH_CMD_LINE_TO));
1724    }
1725
1726    /// Path 1: Border (3 contours: outer frame, inner hollow, separator line).
1727    fn calc_border(&mut self) {
1728        self.vertices.clear();
1729        let bw = self.border_width;
1730        // Outer rectangle
1731        self.vertices
1732            .push((self.x1, self.y1, PATH_CMD_MOVE_TO));
1733        self.vertices
1734            .push((self.x2, self.y1, PATH_CMD_LINE_TO));
1735        self.vertices
1736            .push((self.x2, self.y2, PATH_CMD_LINE_TO));
1737        self.vertices
1738            .push((self.x1, self.y2, PATH_CMD_LINE_TO));
1739        // Inner hollow
1740        self.vertices
1741            .push((self.x1 + bw, self.y1 + bw, PATH_CMD_MOVE_TO));
1742        self.vertices
1743            .push((self.x1 + bw, self.y2 - bw, PATH_CMD_LINE_TO));
1744        self.vertices
1745            .push((self.x2 - bw, self.y2 - bw, PATH_CMD_LINE_TO));
1746        self.vertices
1747            .push((self.x2 - bw, self.y1 + bw, PATH_CMD_LINE_TO));
1748        // Separator line between chart and text
1749        self.vertices
1750            .push((self.xc1 + bw, self.yc2 - bw * 0.5, PATH_CMD_MOVE_TO));
1751        self.vertices
1752            .push((self.xc2 - bw, self.yc2 - bw * 0.5, PATH_CMD_LINE_TO));
1753        self.vertices
1754            .push((self.xc2 - bw, self.yc2 + bw * 0.5, PATH_CMD_LINE_TO));
1755        self.vertices
1756            .push((self.xc1 + bw, self.yc2 + bw * 0.5, PATH_CMD_LINE_TO));
1757    }
1758
1759    /// Path 2: Gamma curve (stroked spline).
1760    fn calc_curve(&mut self) {
1761        self.vertices.clear();
1762        self.gamma_spline
1763            .set_box(self.xs1, self.ys1, self.xs2, self.ys2);
1764
1765        let mut spline_copy = crate::gamma::GammaSpline::new();
1766        // Re-use the same values
1767        let (kx1, ky1, kx2, ky2) = self.gamma_spline.get_values();
1768        spline_copy.set_values(kx1, ky1, kx2, ky2);
1769        spline_copy.set_box(self.xs1, self.ys1, self.xs2, self.ys2);
1770
1771        // Generate spline vertices, then stroke
1772        let mut path_verts = Vec::new();
1773        spline_copy.rewind(0);
1774        loop {
1775            let (mut x, mut y) = (0.0, 0.0);
1776            let cmd = spline_copy.vertex(&mut x, &mut y);
1777            if is_stop(cmd) {
1778                break;
1779            }
1780            path_verts.push((x, y, cmd));
1781        }
1782
1783        // Build a custom vertex source from the captured vertices
1784        let mut path = crate::path_storage::PathStorage::new();
1785        for (i, &(x, y, cmd)) in path_verts.iter().enumerate() {
1786            if i == 0 {
1787                path.move_to(x, y);
1788            } else {
1789                path.line_to(x, y);
1790            }
1791            let _ = cmd;
1792        }
1793
1794        let mut stroke = ConvStroke::new(&mut path);
1795        stroke.set_width(self.curve_width);
1796        stroke.rewind(0);
1797        loop {
1798            let (mut x, mut y) = (0.0, 0.0);
1799            let cmd = stroke.vertex(&mut x, &mut y);
1800            if is_stop(cmd) {
1801                break;
1802            }
1803            self.vertices.push((x, y, cmd));
1804        }
1805    }
1806
1807    /// Path 3: Grid (center lines + crosshairs at control points).
1808    fn calc_grid(&mut self) {
1809        self.vertices.clear();
1810        self.calc_points();
1811
1812        let gw = self.grid_width;
1813        let (xs1, ys1, xs2, ys2) = (self.xs1, self.ys1, self.xs2, self.ys2);
1814        let ymid = (ys1 + ys2) * 0.5;
1815        let xmid = (xs1 + xs2) * 0.5;
1816
1817        // Horizontal center line
1818        self.vertices.push((xs1, ymid - gw * 0.5, PATH_CMD_MOVE_TO));
1819        self.vertices.push((xs2, ymid - gw * 0.5, PATH_CMD_LINE_TO));
1820        self.vertices.push((xs2, ymid + gw * 0.5, PATH_CMD_LINE_TO));
1821        self.vertices.push((xs1, ymid + gw * 0.5, PATH_CMD_LINE_TO));
1822
1823        // Vertical center line
1824        self.vertices.push((xmid - gw * 0.5, ys1, PATH_CMD_MOVE_TO));
1825        self.vertices
1826            .push((xmid - gw * 0.5, ys2, PATH_CMD_LINE_TO));
1827        self.vertices
1828            .push((xmid + gw * 0.5, ys2, PATH_CMD_LINE_TO));
1829        self.vertices
1830            .push((xmid + gw * 0.5, ys1, PATH_CMD_LINE_TO));
1831
1832        // Crosshair at point 1
1833        let (xp1, yp1) = (self.xp1, self.yp1);
1834        self.vertices
1835            .push((xs1, yp1 - gw * 0.5, PATH_CMD_MOVE_TO));
1836        self.vertices
1837            .push((xp1 - gw * 0.5, yp1 - gw * 0.5, PATH_CMD_LINE_TO));
1838        self.vertices
1839            .push((xp1 - gw * 0.5, ys1, PATH_CMD_LINE_TO));
1840        self.vertices
1841            .push((xp1 + gw * 0.5, ys1, PATH_CMD_LINE_TO));
1842        self.vertices
1843            .push((xp1 + gw * 0.5, yp1 + gw * 0.5, PATH_CMD_LINE_TO));
1844        self.vertices
1845            .push((xs1, yp1 + gw * 0.5, PATH_CMD_LINE_TO));
1846
1847        // Crosshair at point 2
1848        let (xp2, yp2) = (self.xp2, self.yp2);
1849        self.vertices
1850            .push((xs2, yp2 + gw * 0.5, PATH_CMD_MOVE_TO));
1851        self.vertices
1852            .push((xp2 + gw * 0.5, yp2 + gw * 0.5, PATH_CMD_LINE_TO));
1853        self.vertices
1854            .push((xp2 + gw * 0.5, ys2, PATH_CMD_LINE_TO));
1855        self.vertices
1856            .push((xp2 - gw * 0.5, ys2, PATH_CMD_LINE_TO));
1857        self.vertices
1858            .push((xp2 - gw * 0.5, yp2 - gw * 0.5, PATH_CMD_LINE_TO));
1859        self.vertices
1860            .push((xs2, yp2 - gw * 0.5, PATH_CMD_LINE_TO));
1861    }
1862
1863    /// Path 4: Inactive control point (ellipse).
1864    fn calc_inactive_point(&mut self) {
1865        self.vertices.clear();
1866        self.calc_points();
1867        let (cx, cy) = if self.p1_active {
1868            (self.xp2, self.yp2)
1869        } else {
1870            (self.xp1, self.yp1)
1871        };
1872        let mut ell = Ellipse::new(cx, cy, self.point_size, self.point_size, 32, false);
1873        ell.rewind(0);
1874        loop {
1875            let (mut x, mut y) = (0.0, 0.0);
1876            let cmd = ell.vertex(&mut x, &mut y);
1877            if is_stop(cmd) {
1878                break;
1879            }
1880            self.vertices.push((x, y, cmd));
1881        }
1882    }
1883
1884    /// Path 5: Active control point (ellipse).
1885    fn calc_active_point(&mut self) {
1886        self.vertices.clear();
1887        self.calc_points();
1888        let (cx, cy) = if self.p1_active {
1889            (self.xp1, self.yp1)
1890        } else {
1891            (self.xp2, self.yp2)
1892        };
1893        let mut ell = Ellipse::new(cx, cy, self.point_size, self.point_size, 32, false);
1894        ell.rewind(0);
1895        loop {
1896            let (mut x, mut y) = (0.0, 0.0);
1897            let cmd = ell.vertex(&mut x, &mut y);
1898            if is_stop(cmd) {
1899                break;
1900            }
1901            self.vertices.push((x, y, cmd));
1902        }
1903    }
1904
1905    /// Path 6: Text display showing current kx1, ky1, kx2, ky2 values.
1906    fn calc_text_display(&mut self) {
1907        self.vertices.clear();
1908        let (kx1, ky1, kx2, ky2) = self.gamma_spline.get_values();
1909        let text = format!("{:.3} {:.3} {:.3} {:.3}", kx1, ky1, kx2, ky2);
1910
1911        let mut txt = GsvText::new();
1912        txt.text(&text);
1913        txt.size(self.text_height, self.text_width);
1914        txt.start_point(
1915            self.xt1 + self.border_width * 2.0,
1916            (self.yt1 + self.y2) * 0.5 - self.text_height * 0.5,
1917        );
1918
1919        let mut stroke = ConvStroke::new(txt);
1920        stroke.set_width(self.text_thickness);
1921        stroke.set_line_join(LineJoin::Round);
1922        stroke.set_line_cap(LineCap::Round);
1923
1924        stroke.rewind(0);
1925        loop {
1926            let (mut x, mut y) = (0.0, 0.0);
1927            let cmd = stroke.vertex(&mut x, &mut y);
1928            if is_stop(cmd) {
1929                break;
1930            }
1931            self.vertices.push((x, y, cmd));
1932        }
1933    }
1934}
1935
1936impl Ctrl for GammaCtrl {
1937    fn num_paths(&self) -> u32 {
1938        7
1939    }
1940
1941    fn color(&self, path_id: u32) -> Rgba8 {
1942        self.colors[path_id.min(6) as usize]
1943    }
1944}
1945
1946impl VertexSource for GammaCtrl {
1947    fn rewind(&mut self, path_id: u32) {
1948        self.vertex_idx = 0;
1949        match path_id {
1950            0 => self.calc_background(),
1951            1 => self.calc_border(),
1952            2 => self.calc_curve(),
1953            3 => self.calc_grid(),
1954            4 => self.calc_inactive_point(),
1955            5 => self.calc_active_point(),
1956            6 => self.calc_text_display(),
1957            _ => {
1958                self.vertices.clear();
1959            }
1960        }
1961    }
1962
1963    fn vertex(&mut self, x: &mut f64, y: &mut f64) -> u32 {
1964        if self.vertex_idx < self.vertices.len() {
1965            let (vx, vy, cmd) = self.vertices[self.vertex_idx];
1966            *x = vx;
1967            *y = vy;
1968            self.vertex_idx += 1;
1969            cmd
1970        } else {
1971            PATH_CMD_STOP
1972        }
1973    }
1974}
1975
1976// ============================================================================
1977// Tests
1978// ============================================================================
1979
1980#[cfg(test)]
1981mod tests {
1982    use super::*;
1983
1984    #[test]
1985    fn test_slider_ctrl_default_value() {
1986        let s = SliderCtrl::new(5.0, 5.0, 300.0, 12.0);
1987        // Default normalized value is 0.5, range 0..1
1988        assert!((s.value() - 0.5).abs() < 1e-10);
1989    }
1990
1991    #[test]
1992    fn test_slider_ctrl_set_value() {
1993        let mut s = SliderCtrl::new(5.0, 5.0, 300.0, 12.0);
1994        s.range(-180.0, 180.0);
1995        s.set_value(90.0);
1996        assert!((s.value() - 90.0).abs() < 1e-6);
1997    }
1998
1999    #[test]
2000    fn test_slider_ctrl_vertex_source() {
2001        let mut s = SliderCtrl::new(5.0, 5.0, 300.0, 12.0);
2002        // Path 0: background rectangle — should have 4 vertices
2003        s.rewind(0);
2004        let mut count = 0;
2005        loop {
2006            let (mut x, mut y) = (0.0, 0.0);
2007            let cmd = s.vertex(&mut x, &mut y);
2008            if is_stop(cmd) {
2009                break;
2010            }
2011            count += 1;
2012        }
2013        assert_eq!(count, 4);
2014    }
2015
2016    #[test]
2017    fn test_slider_ctrl_pointer_circle() {
2018        let mut s = SliderCtrl::new(5.0, 5.0, 300.0, 12.0);
2019        // Path 4: pointer circle — should have ~33 vertices (32 segments + close)
2020        s.rewind(4);
2021        let mut count = 0;
2022        loop {
2023            let (mut x, mut y) = (0.0, 0.0);
2024            let cmd = s.vertex(&mut x, &mut y);
2025            if is_stop(cmd) {
2026                break;
2027            }
2028            count += 1;
2029        }
2030        assert!(count >= 32);
2031    }
2032
2033    #[test]
2034    fn test_slider_ctrl_text() {
2035        let mut s = SliderCtrl::new(5.0, 5.0, 300.0, 12.0);
2036        s.label("Angle=%3.2f");
2037        s.set_value(45.0);
2038        // Path 2: text — should produce some vertices
2039        s.rewind(2);
2040        let mut count = 0;
2041        loop {
2042            let (mut x, mut y) = (0.0, 0.0);
2043            let cmd = s.vertex(&mut x, &mut y);
2044            if is_stop(cmd) {
2045                break;
2046            }
2047            count += 1;
2048        }
2049        assert!(count > 0);
2050    }
2051
2052    #[test]
2053    fn test_slider_ctrl_num_paths() {
2054        let s = SliderCtrl::new(5.0, 5.0, 300.0, 12.0);
2055        assert_eq!(s.num_paths(), 6);
2056    }
2057
2058    #[test]
2059    fn test_scale_ctrl_defaults() {
2060        let s = ScaleCtrl::new(5.0, 5.0, 395.0, 12.0);
2061        assert!((s.value1() - 0.3).abs() < 1e-10);
2062        assert!((s.value2() - 0.7).abs() < 1e-10);
2063        assert_eq!(s.num_paths(), 5);
2064    }
2065
2066    #[test]
2067    fn test_scale_ctrl_setters_clamp_and_gap() {
2068        let mut s = ScaleCtrl::new(5.0, 5.0, 395.0, 12.0);
2069        s.set_min_delta(0.1);
2070        s.set_value1(0.95);
2071        assert!(s.value1() <= s.value2() - 0.1 + 1e-10);
2072        s.set_value2(-1.0);
2073        assert!(s.value2() >= s.value1() + 0.1 - 1e-10);
2074    }
2075
2076    #[test]
2077    fn test_scale_ctrl_paths_emit_vertices() {
2078        let mut s = ScaleCtrl::new(5.0, 5.0, 395.0, 12.0);
2079        for path_id in 0..5 {
2080            s.rewind(path_id);
2081            let mut seen = 0;
2082            loop {
2083                let (mut x, mut y) = (0.0, 0.0);
2084                let cmd = s.vertex(&mut x, &mut y);
2085                if is_stop(cmd) {
2086                    break;
2087                }
2088                seen += 1;
2089            }
2090            assert!(seen > 0, "expected vertices for path {}", path_id);
2091        }
2092    }
2093
2094    #[test]
2095    fn test_sprintf_format_various() {
2096        // %1.0f — used by trans_polar demo "Some Value=%1.0f"
2097        assert_eq!(sprintf_format("Some Value=%1.0f", 40.0), "Some Value=40");
2098        assert_eq!(sprintf_format("Some Value=%1.0f", 32.0), "Some Value=32");
2099
2100        // %.3f — "Spiral=%.3f"
2101        assert_eq!(sprintf_format("Spiral=%.3f", 0.0), "Spiral=0.000");
2102        assert_eq!(sprintf_format("Spiral=%.3f", 0.05), "Spiral=0.050");
2103
2104        // %.2f — "N=%.2f"
2105        assert_eq!(sprintf_format("N=%.2f", 4.0), "N=4.00");
2106
2107        // %3.2f — "Angle=%3.2f"
2108        assert_eq!(sprintf_format("Angle=%3.2f", 45.0), "Angle=45.00");
2109
2110        // %.0f — "Num Points=%.0f"
2111        assert_eq!(sprintf_format("Num Points=%.0f", 200.0), "Num Points=200");
2112
2113        // %d — integer format
2114        assert_eq!(sprintf_format("Count=%d", 42.7), "Count=42");
2115
2116        // %4.3f — "radius=%4.3f"
2117        assert_eq!(sprintf_format("radius=%4.3f", 1.5), "radius=1.500");
2118
2119        // No format specifier
2120        assert_eq!(sprintf_format("Hello", 42.0), "Hello");
2121
2122        // %f without precision (default 6 digits)
2123        assert_eq!(sprintf_format("Val=%f", 3.14), "Val=3.140000");
2124    }
2125
2126    #[test]
2127    fn test_cbox_ctrl_basic() {
2128        let mut c = CboxCtrl::new(10.0, 10.0, "Outline");
2129        assert!(!c.status());
2130        c.set_status(true);
2131        assert!(c.status());
2132        assert_eq!(c.num_paths(), 3);
2133    }
2134
2135    #[test]
2136    fn test_cbox_ctrl_checkmark_only_when_active() {
2137        let mut c = CboxCtrl::new(10.0, 10.0, "Test");
2138        // Path 2 (checkmark) should have 0 vertices when inactive
2139        c.set_status(false);
2140        c.rewind(2);
2141        let (mut x, mut y) = (0.0, 0.0);
2142        let cmd = c.vertex(&mut x, &mut y);
2143        assert_eq!(cmd, PATH_CMD_STOP);
2144
2145        // Should have vertices when active
2146        c.set_status(true);
2147        c.rewind(2);
2148        let cmd = c.vertex(&mut x, &mut y);
2149        assert_eq!(cmd, PATH_CMD_MOVE_TO);
2150    }
2151
2152    #[test]
2153    fn test_rbox_ctrl_basic() {
2154        let mut r = RboxCtrl::new(10.0, 10.0, 150.0, 100.0);
2155        r.add_item("Option A");
2156        r.add_item("Option B");
2157        r.add_item("Option C");
2158        r.set_cur_item(1);
2159        assert_eq!(r.cur_item(), 1);
2160        assert_eq!(r.num_paths(), 5);
2161    }
2162
2163    #[test]
2164    fn test_rbox_ctrl_inactive_circles() {
2165        let mut r = RboxCtrl::new(10.0, 10.0, 150.0, 100.0);
2166        r.add_item("A");
2167        r.add_item("B");
2168        // Path 3: inactive circles — should produce vertices for 2 items
2169        r.rewind(3);
2170        let mut count = 0;
2171        loop {
2172            let (mut x, mut y) = (0.0, 0.0);
2173            let cmd = r.vertex(&mut x, &mut y);
2174            if is_stop(cmd) {
2175                break;
2176            }
2177            count += 1;
2178        }
2179        assert!(count > 32); // At least 2 circles worth
2180    }
2181
2182    #[test]
2183    fn test_rbox_ctrl_no_active_when_negative() {
2184        let mut r = RboxCtrl::new(10.0, 10.0, 150.0, 100.0);
2185        r.add_item("X");
2186        // cur_item = -1 means no selection
2187        r.rewind(4);
2188        let (mut x, mut y) = (0.0, 0.0);
2189        let cmd = r.vertex(&mut x, &mut y);
2190        assert_eq!(cmd, PATH_CMD_STOP);
2191    }
2192
2193    // ====================================================================
2194    // GammaCtrl tests
2195    // ====================================================================
2196
2197    #[test]
2198    fn test_gamma_ctrl_basic() {
2199        let gc = GammaCtrl::new(10.0, 10.0, 200.0, 200.0);
2200        assert_eq!(gc.num_paths(), 7);
2201    }
2202
2203    #[test]
2204    fn test_gamma_ctrl_default_values() {
2205        let gc = GammaCtrl::new(10.0, 10.0, 200.0, 200.0);
2206        let (kx1, ky1, kx2, ky2) = gc.get_values();
2207        assert!((kx1 - 1.0).abs() < 0.01);
2208        assert!((ky1 - 1.0).abs() < 0.01);
2209        assert!((kx2 - 1.0).abs() < 0.01);
2210        assert!((ky2 - 1.0).abs() < 0.01);
2211    }
2212
2213    #[test]
2214    fn test_gamma_ctrl_set_values() {
2215        let mut gc = GammaCtrl::new(10.0, 10.0, 200.0, 200.0);
2216        gc.set_values(0.5, 1.5, 0.8, 1.2);
2217        let (kx1, ky1, kx2, ky2) = gc.get_values();
2218        assert!((kx1 - 0.5).abs() < 0.001);
2219        assert!((ky1 - 1.5).abs() < 0.001);
2220        assert!((kx2 - 0.8).abs() < 0.001);
2221        assert!((ky2 - 1.2).abs() < 0.001);
2222    }
2223
2224    #[test]
2225    fn test_gamma_ctrl_gamma_table() {
2226        let gc = GammaCtrl::new(10.0, 10.0, 200.0, 200.0);
2227        let gamma = gc.gamma();
2228        assert_eq!(gamma[0], 0);
2229        assert_eq!(gamma[255], 255);
2230    }
2231
2232    #[test]
2233    fn test_gamma_ctrl_background_path() {
2234        let mut gc = GammaCtrl::new(10.0, 10.0, 200.0, 200.0);
2235        gc.rewind(0);
2236        let mut count = 0;
2237        loop {
2238            let (mut x, mut y) = (0.0, 0.0);
2239            let cmd = gc.vertex(&mut x, &mut y);
2240            if is_stop(cmd) {
2241                break;
2242            }
2243            count += 1;
2244        }
2245        assert_eq!(count, 4); // Rectangle
2246    }
2247
2248    #[test]
2249    fn test_gamma_ctrl_border_path() {
2250        let mut gc = GammaCtrl::new(10.0, 10.0, 200.0, 200.0);
2251        gc.rewind(1);
2252        let mut count = 0;
2253        loop {
2254            let (mut x, mut y) = (0.0, 0.0);
2255            let cmd = gc.vertex(&mut x, &mut y);
2256            if is_stop(cmd) {
2257                break;
2258            }
2259            count += 1;
2260        }
2261        assert_eq!(count, 12); // 3 contours * 4 vertices
2262    }
2263
2264    #[test]
2265    fn test_gamma_ctrl_curve_path() {
2266        let mut gc = GammaCtrl::new(10.0, 10.0, 200.0, 200.0);
2267        gc.rewind(2);
2268        let mut count = 0;
2269        loop {
2270            let (mut x, mut y) = (0.0, 0.0);
2271            let cmd = gc.vertex(&mut x, &mut y);
2272            if is_stop(cmd) {
2273                break;
2274            }
2275            count += 1;
2276        }
2277        assert!(count > 10, "Curve path should have many vertices, got {count}");
2278    }
2279
2280    #[test]
2281    fn test_gamma_ctrl_grid_path() {
2282        let mut gc = GammaCtrl::new(10.0, 10.0, 200.0, 200.0);
2283        gc.rewind(3);
2284        let mut count = 0;
2285        loop {
2286            let (mut x, mut y) = (0.0, 0.0);
2287            let cmd = gc.vertex(&mut x, &mut y);
2288            if is_stop(cmd) {
2289                break;
2290            }
2291            count += 1;
2292        }
2293        assert_eq!(count, 20); // 4 contours: 4+4+6+6
2294    }
2295
2296    #[test]
2297    fn test_gamma_ctrl_points_path() {
2298        let mut gc = GammaCtrl::new(10.0, 10.0, 200.0, 200.0);
2299        // Path 4: inactive point (ellipse with 32 segments)
2300        gc.rewind(4);
2301        let mut count = 0;
2302        loop {
2303            let (mut x, mut y) = (0.0, 0.0);
2304            let cmd = gc.vertex(&mut x, &mut y);
2305            if is_stop(cmd) {
2306                break;
2307            }
2308            count += 1;
2309        }
2310        assert!(count >= 32);
2311
2312        // Path 5: active point
2313        gc.rewind(5);
2314        let mut count = 0;
2315        loop {
2316            let (mut x, mut y) = (0.0, 0.0);
2317            let cmd = gc.vertex(&mut x, &mut y);
2318            if is_stop(cmd) {
2319                break;
2320            }
2321            count += 1;
2322        }
2323        assert!(count >= 32);
2324    }
2325
2326    #[test]
2327    fn test_gamma_ctrl_text_path() {
2328        let mut gc = GammaCtrl::new(10.0, 10.0, 200.0, 200.0);
2329        gc.rewind(6);
2330        let mut count = 0;
2331        loop {
2332            let (mut x, mut y) = (0.0, 0.0);
2333            let cmd = gc.vertex(&mut x, &mut y);
2334            if is_stop(cmd) {
2335                break;
2336            }
2337            count += 1;
2338        }
2339        assert!(count > 0, "Text path should have vertices");
2340    }
2341
2342    #[test]
2343    fn test_gamma_ctrl_mouse_interaction() {
2344        let mut gc = GammaCtrl::new(0.0, 0.0, 200.0, 200.0);
2345        // in_rect
2346        assert!(gc.in_rect(100.0, 100.0));
2347        assert!(!gc.in_rect(300.0, 300.0));
2348
2349        // Arrow key adjustment
2350        let changed = gc.on_arrow_keys(false, true, false, false);
2351        assert!(changed);
2352        let (kx1, _, _, _) = gc.get_values();
2353        assert!((kx1 - 1.005).abs() < 0.001);
2354    }
2355}