Skip to main content

minifb_ui/window/
mod.rs

1use std::sync::{Arc, Mutex};
2
3struct CharCallback {
4    buffer: Arc<Mutex<Vec<char>>>,
5}
6
7impl minifb::InputCallback for CharCallback {
8    fn add_char(&mut self, uni_char: u32) {
9        if let Some(c) = char::from_u32(uni_char) {
10            if let Ok(mut buf) = self.buffer.lock() {
11                buf.push(c);
12            }
13        }
14    }
15}
16
17/// Owns the window and the raw flat framebuffer to be rendered.
18pub struct Window {
19    pub window: minifb::Window,
20    pub width: usize,
21    pub height: usize,
22    pub framebuffer_raw: Vec<u32>,
23    pub is_fullscreen: bool,
24    /// Anti-aliasing softness in pixels. 0.0 = hard edges (no AA), 1.0 = standard AA.
25    /// Higher values produce softer/blurrier edges.
26    pub aa: f32,
27    input_buffer: Arc<Mutex<Vec<char>>>,
28    clip_stack: Vec<(usize, usize, usize, usize)>,
29}
30
31impl Window {
32    /// Creates a non-resizable window with a resolution of 1280 by 720 pixels
33    pub fn default() -> Self {
34        let input_buffer = Arc::new(Mutex::new(Vec::new()));
35        let mut window = minifb::Window::new(
36            "minifb-ui",
37            1280,
38            720,
39            minifb::WindowOptions {
40                resize: false,
41                scale: minifb::Scale::X1,
42                scale_mode: minifb::ScaleMode::AspectRatioStretch,
43                ..Default::default()
44            },
45        )
46        .unwrap();
47        window.set_input_callback(Box::new(CharCallback {
48            buffer: Arc::clone(&input_buffer),
49        }));
50        Self {
51            window,
52            width: 1280,
53            height: 720,
54            framebuffer_raw: vec![0u32; 1280 * 720],
55            is_fullscreen: false,
56            aa: 1.0,
57            input_buffer,
58            clip_stack: Vec::new(),
59        }
60    }
61
62    /// Creates a window with custom resolution, can be borderless and resizable
63    pub fn custom(name: &str, width: usize, height: usize, borders: bool, resizable: bool) -> Self {
64        let input_buffer = Arc::new(Mutex::new(Vec::new()));
65        let mut window = minifb::Window::new(
66            name,
67            width,
68            height,
69            minifb::WindowOptions {
70                resize: resizable,
71                scale: minifb::Scale::X1,
72                scale_mode: minifb::ScaleMode::Center,
73                borderless: borders,
74                ..Default::default()
75            },
76        )
77        .unwrap();
78        window.set_input_callback(Box::new(CharCallback {
79            buffer: Arc::clone(&input_buffer),
80        }));
81        Self {
82            window,
83            width,
84            height,
85            framebuffer_raw: vec![0u32; width * height],
86            is_fullscreen: false,
87            aa: 1.0,
88            input_buffer,
89            clip_stack: Vec::new(),
90        }
91    }
92
93    /// Drains and returns all characters typed since the last call.
94    pub fn get_typed_chars(&self) -> Vec<char> {
95        if let Ok(mut buf) = self.input_buffer.lock() {
96            buf.drain(..).collect()
97        } else {
98            Vec::new()
99        }
100    }
101
102    /// Draws a single pixel at given coordinates with color, respecting alpha
103    pub fn draw_pixel(&mut self, x: usize, y: usize, color: &crate::color::Color) {
104        if x >= self.width || y >= self.height {
105            return;
106        }
107        let alpha = color.a as u32;
108        if alpha == 0 {
109            return;
110        }
111        let idx = y * self.width + x;
112        if alpha == 255 {
113            self.framebuffer_raw[idx] = color.as_u32();
114            return;
115        }
116        let bg = self.framebuffer_raw[idx];
117        let bg_r = (bg >> 16) & 0xFF;
118        let bg_g = (bg >> 8) & 0xFF;
119        let bg_b = bg & 0xFF;
120        let inv = 255 - alpha;
121        let r = (color.r as u32 * alpha + bg_r * inv) / 255;
122        let g = (color.g as u32 * alpha + bg_g * inv) / 255;
123        let b = (color.b as u32 * alpha + bg_b * inv) / 255;
124        self.framebuffer_raw[idx] = (r << 16) | (g << 8) | b;
125    }
126
127    pub fn get_pixel(&self, x: usize, y: usize) -> u32 {
128        self.framebuffer_raw[y * self.width + x]
129    }
130
131    pub fn clear(&mut self, color: &crate::color::Color) {
132        self.framebuffer_raw.fill(color.as_u32());
133    }
134
135    // ─── Lines ──────────────────────────────────────────────────────────
136
137    /// Draws a straight line. Uses AA when `self.aa > 0`.
138    pub fn draw_line(
139        &mut self,
140        x0: isize,
141        y0: isize,
142        x1: isize,
143        y1: isize,
144        th: usize,
145        color: crate::color::Color,
146    ) {
147        if self.aa > 0.0 {
148            self.draw_line_aa_impl(x0 as f32, y0 as f32, x1 as f32, y1 as f32, th as f32, &color);
149        } else {
150            self.draw_line_aliased(x0, y0, x1, y1, th, &color);
151        }
152    }
153
154    /// Draws a dashed line. `dash_len` is pixels on, `gap_len` is pixels off.
155    pub fn draw_line_dashed(
156        &mut self,
157        x0: isize,
158        y0: isize,
159        x1: isize,
160        y1: isize,
161        th: usize,
162        dash_len: usize,
163        gap_len: usize,
164        color: crate::color::Color,
165    ) {
166        let dx = (x1 - x0) as f32;
167        let dy = (y1 - y0) as f32;
168        let total_len = (dx * dx + dy * dy).sqrt();
169        if total_len < 0.5 {
170            return;
171        }
172        let dir_x = dx / total_len;
173        let dir_y = dy / total_len;
174        let pattern_len = (dash_len + gap_len) as f32;
175
176        let mut dist = 0.0f32;
177        while dist < total_len {
178            let seg_end = (dist + dash_len as f32).min(total_len);
179            let sx = x0 as f32 + dir_x * dist;
180            let sy = y0 as f32 + dir_y * dist;
181            let ex = x0 as f32 + dir_x * seg_end;
182            let ey = y0 as f32 + dir_y * seg_end;
183
184            if self.aa > 0.0 {
185                self.draw_line_aa_impl(sx, sy, ex, ey, th as f32, &color);
186            } else {
187                self.draw_line_aliased(sx as isize, sy as isize, ex as isize, ey as isize, th, &color);
188            }
189
190            dist += pattern_len;
191        }
192    }
193
194    /// Draws a dotted line (dash=1, gap=spacing)
195    pub fn draw_line_dotted(
196        &mut self,
197        x0: isize,
198        y0: isize,
199        x1: isize,
200        y1: isize,
201        th: usize,
202        spacing: usize,
203        color: crate::color::Color,
204    ) {
205        self.draw_line_dashed(x0, y0, x1, y1, th, 1, spacing, color);
206    }
207
208    // ─── Rectangles ─────────────────────────────────────────────────────
209
210    /// Draws a filled rectangle with optional rounded corners, alpha blending, and background blur.
211    /// Pass `radius: 0` for sharp corners, `blur: 0` for no blur.
212    pub fn draw_rect_f(&mut self, x: usize, y: usize, w: usize, h: usize, radius: usize, color: &crate::color::Color, blur: usize) {
213        if radius > 0 {
214            if blur > 0 {
215                self.blur_region_rounded(x, y, w, h, radius, blur);
216            }
217            if color.a == 0 {
218                return;
219            }
220            let base_alpha = color.a as f32 / 255.0;
221            if self.aa > 0.0 {
222                self.draw_rounded_rect_f_sdf(x as f32, y as f32, w as f32, h as f32, radius as f32, color, base_alpha);
223            } else if color.a == 255 {
224                self.draw_rounded_rect_f_aliased(x, y, w, h, radius, color);
225            } else {
226                self.draw_rounded_rect_f_sdf(x as f32, y as f32, w as f32, h as f32, radius as f32, color, base_alpha);
227            }
228        } else {
229            if blur > 0 {
230                self.blur_region(x, y, w, h, blur);
231            }
232            let alpha = color.a as u32;
233            if alpha == 0 {
234                return;
235            }
236            if alpha == 255 {
237                let value = color.as_u32();
238                let start = y * self.width + x;
239                for dy in 0..h {
240                    let row = &mut self.framebuffer_raw[start + dy * self.width..][..w];
241                    row.fill(value);
242                }
243                return;
244            }
245            let fg_r = color.r as u32;
246            let fg_g = color.g as u32;
247            let fg_b = color.b as u32;
248            let inv = 255 - alpha;
249            for dy in 0..h {
250                let row = (y + dy) * self.width + x;
251                for dx in 0..w {
252                    let idx = row + dx;
253                    let bg = self.framebuffer_raw[idx];
254                    let bg_r = (bg >> 16) & 0xFF;
255                    let bg_g = (bg >> 8) & 0xFF;
256                    let bg_b = bg & 0xFF;
257                    let r = (fg_r * alpha + bg_r * inv) / 255;
258                    let g = (fg_g * alpha + bg_g * inv) / 255;
259                    let b = (fg_b * alpha + bg_b * inv) / 255;
260                    self.framebuffer_raw[idx] = (r << 16) | (g << 8) | b;
261                }
262            }
263        }
264    }
265
266    /// Draws a hollow rectangle with optional rounded corners.
267    /// Pass `radius: 0` for sharp corners.
268    pub fn draw_rect(&mut self, x: usize, y: usize, w: usize, h: usize, radius: usize, color: &crate::color::Color) {
269        if radius > 0 {
270            if self.aa > 0.0 {
271                self.draw_rounded_rect_sdf(x as f32, y as f32, w as f32, h as f32, radius as f32, 1.0, color);
272            } else {
273                self.draw_rounded_rect_aliased(x, y, w, h, radius, color);
274            }
275        } else {
276            let value = color.as_u32();
277            if h >= 1 {
278                let top = y * self.width + x;
279                self.framebuffer_raw[top..top + w].fill(value);
280            }
281            if h >= 2 {
282                let bottom = (y + h - 1) * self.width + x;
283                self.framebuffer_raw[bottom..bottom + w].fill(value);
284            }
285            if w >= 2 && h >= 2 {
286                let left_start = (y + 1) * self.width + x;
287                let right_start = (y + 1) * self.width + x + w - 1;
288                for dy in 0..(h - 2) {
289                    let offset = dy * self.width;
290                    self.framebuffer_raw[left_start + offset] = value;
291                    self.framebuffer_raw[right_start + offset] = value;
292                }
293            }
294        }
295    }
296
297    // ─── Ellipses (and circles) ───────────────────────────────────────────
298
299    /// Draws a filled ellipse with alpha blending and optional background blur.
300    /// For circles, pass `rx == ry`. Pass `blur: 0` for no blur.
301    pub fn draw_ellipse_f(
302        &mut self,
303        cx: isize,
304        cy: isize,
305        rx: usize,
306        ry: usize,
307        color: &crate::color::Color,
308        blur: usize,
309    ) {
310        if blur > 0 {
311            let bx = (cx - rx as isize).max(0) as usize;
312            let by = (cy - ry as isize).max(0) as usize;
313            let bw = (rx * 2).min(self.width.saturating_sub(bx));
314            let bh = (ry * 2).min(self.height.saturating_sub(by));
315            self.blur_region(bx, by, bw, bh, blur);
316        }
317        if color.a == 0 {
318            return;
319        }
320        let base_alpha = color.a as f32 / 255.0;
321        if rx == ry {
322            if self.aa > 0.0 {
323                self.draw_circle_f_sdf(cx as f32, cy as f32, rx as f32, color, base_alpha);
324            } else if color.a == 255 {
325                self.draw_circle_f_aliased(cx, cy, rx, color);
326            } else {
327                self.draw_circle_f_sdf(cx as f32, cy as f32, rx as f32, color, base_alpha);
328            }
329        } else {
330            if self.aa > 0.0 {
331                self.draw_ellipse_f_sdf(cx as f32, cy as f32, rx as f32, ry as f32, color, base_alpha);
332            } else if color.a == 255 {
333                self.draw_ellipse_f_aliased(cx, cy, rx, ry, color);
334            } else {
335                self.draw_ellipse_f_sdf(cx as f32, cy as f32, rx as f32, ry as f32, color, base_alpha);
336            }
337        }
338    }
339
340    /// Draws a hollow ellipse. For circles, pass `rx == ry`.
341    pub fn draw_ellipse(
342        &mut self,
343        cx: isize,
344        cy: isize,
345        rx: usize,
346        ry: usize,
347        color: &crate::color::Color,
348    ) {
349        if rx == ry {
350            if self.aa > 0.0 {
351                self.draw_circle_sdf(cx as f32, cy as f32, rx as f32, 1.0, color);
352            } else {
353                self.draw_circle_aliased(cx, cy, rx, color);
354            }
355        } else {
356            if self.aa > 0.0 {
357                self.draw_ellipse_sdf(cx as f32, cy as f32, rx as f32, ry as f32, 1.0, color);
358            } else {
359                self.draw_ellipse_aliased(cx, cy, rx, ry, color);
360            }
361        }
362    }
363
364    // ─── Gradients ──────────────────────────────────────────────────────
365
366    /// Draws a filled rectangle with a horizontal linear gradient (left to right)
367    /// Draws a filled rectangle with a horizontal linear gradient (left to right).
368    /// Pass `radius: 0` for sharp corners.
369    pub fn draw_gradient_h(
370        &mut self,
371        x: usize,
372        y: usize,
373        w: usize,
374        h: usize,
375        radius: usize,
376        color_left: &crate::color::Color,
377        color_right: &crate::color::Color,
378    ) {
379        if w == 0 || h == 0 {
380            return;
381        }
382        if radius > 0 {
383            let aa = self.aa;
384            let fx = x as f32;
385            let fy = y as f32;
386            let fw = w as f32;
387            let fh = h as f32;
388            let r = (radius as f32).min(fw / 2.0).min(fh / 2.0);
389
390            let min_px = ((fx - 1.0).floor() as isize).max(0);
391            let max_px = ((fx + fw + 1.0).ceil() as isize).min(self.width as isize - 1);
392            let min_py = ((fy - 1.0).floor() as isize).max(0);
393            let max_py = ((fy + fh + 1.0).ceil() as isize).min(self.height as isize - 1);
394
395            for py in min_py..=max_py {
396                for px in min_px..=max_px {
397                    let pfx = px as f32 + 0.5;
398                    let pfy = py as f32 + 0.5;
399                    let t = ((pfx - fx) / (fw - 1.0).max(1.0)).clamp(0.0, 1.0);
400                    let col = color_left.lerp(color_right, t);
401                    if aa > 0.0 {
402                        let dist = rounded_rect_sdf(pfx, pfy, fx, fy, fw, fh, r);
403                        let coverage = (aa * 0.5 - dist).clamp(0.0, 1.0) / aa;
404                        if coverage > 0.0 {
405                            self.blend_pixel(px, py, &col, coverage);
406                        }
407                    } else {
408                        self.blend_pixel(px, py, &col, 1.0);
409                    }
410                }
411            }
412        } else {
413            for dy in 0..h {
414                let row = (y + dy) * self.width;
415                for dx in 0..w {
416                    let t = dx as f32 / (w - 1).max(1) as f32;
417                    let r = (color_left.r as f32 * (1.0 - t) + color_right.r as f32 * t) as u32;
418                    let g = (color_left.g as f32 * (1.0 - t) + color_right.g as f32 * t) as u32;
419                    let b = (color_left.b as f32 * (1.0 - t) + color_right.b as f32 * t) as u32;
420                    self.framebuffer_raw[row + x + dx] = (r << 16) | (g << 8) | b;
421                }
422            }
423        }
424    }
425
426    /// Draws a filled rectangle with a vertical linear gradient (top to bottom).
427    /// Pass `radius: 0` for sharp corners.
428    pub fn draw_gradient_v(
429        &mut self,
430        x: usize,
431        y: usize,
432        w: usize,
433        h: usize,
434        radius: usize,
435        color_top: &crate::color::Color,
436        color_bottom: &crate::color::Color,
437    ) {
438        if w == 0 || h == 0 {
439            return;
440        }
441        if radius > 0 {
442            let aa = self.aa;
443            let fx = x as f32;
444            let fy = y as f32;
445            let fw = w as f32;
446            let fh = h as f32;
447            let r = (radius as f32).min(fw / 2.0).min(fh / 2.0);
448
449            if aa > 0.0 {
450                let min_px = ((fx - 1.0).floor() as isize).max(0);
451                let max_px = ((fx + fw + 1.0).ceil() as isize).min(self.width as isize - 1);
452                let min_py = ((fy - 1.0).floor() as isize).max(0);
453                let max_py = ((fy + fh + 1.0).ceil() as isize).min(self.height as isize - 1);
454
455                for py in min_py..=max_py {
456                    let t = (py as f32 - fy) / (fh - 1.0).max(1.0);
457                    let t = t.clamp(0.0, 1.0);
458                    let row_color = color_top.lerp(color_bottom, t);
459                    for px in min_px..=max_px {
460                        let pfx = px as f32 + 0.5;
461                        let pfy = py as f32 + 0.5;
462                        let dist = rounded_rect_sdf(pfx, pfy, fx, fy, fw, fh, r);
463                        let coverage = (aa * 0.5 - dist).clamp(0.0, 1.0) / aa;
464                        if coverage > 0.0 {
465                            self.blend_pixel(px, py, &row_color, coverage);
466                        }
467                    }
468                }
469            } else {
470                let ri = radius.min(w / 2).min(h / 2);
471                for dy in 0..h {
472                    let t = dy as f32 / (h - 1).max(1) as f32;
473                    let cr = (color_top.r as f32 * (1.0 - t) + color_bottom.r as f32 * t) as u32;
474                    let cg = (color_top.g as f32 * (1.0 - t) + color_bottom.g as f32 * t) as u32;
475                    let cb = (color_top.b as f32 * (1.0 - t) + color_bottom.b as f32 * t) as u32;
476                    let value = (cr << 16) | (cg << 8) | cb;
477
478                    let (row_start, row_end) = if dy < ri {
479                        let cy = ri - dy;
480                        let dx = ri - isqrt(ri * ri - cy * cy);
481                        (x + dx, x + w - dx)
482                    } else if dy >= h - ri {
483                        let cy = dy - (h - 1 - ri);
484                        let dx = ri - isqrt(ri * ri - cy * cy);
485                        (x + dx, x + w - dx)
486                    } else {
487                        (x, x + w)
488                    };
489
490                    let start = (y + dy) * self.width + row_start;
491                    let len = row_end - row_start;
492                    self.framebuffer_raw[start..start + len].fill(value);
493                }
494            }
495        } else {
496            for dy in 0..h {
497                let t = dy as f32 / (h - 1).max(1) as f32;
498                let r = (color_top.r as f32 * (1.0 - t) + color_bottom.r as f32 * t) as u32;
499                let g = (color_top.g as f32 * (1.0 - t) + color_bottom.g as f32 * t) as u32;
500                let b = (color_top.b as f32 * (1.0 - t) + color_bottom.b as f32 * t) as u32;
501                let value = (r << 16) | (g << 8) | b;
502                let row = (y + dy) * self.width + x;
503                self.framebuffer_raw[row..row + w].fill(value);
504            }
505        }
506    }
507
508    // ─── Box Shadow ─────────────────────────────────────────────────────
509
510    /// Draws a rectangular drop shadow.
511    /// Draws a drop shadow with optional rounded corners using SDF distance falloff.
512    /// Pass `radius: 0` for sharp corners.
513    pub fn draw_box_shadow(
514        &mut self,
515        x: isize,
516        y: isize,
517        w: usize,
518        h: usize,
519        radius: usize,
520        offset_x: isize,
521        offset_y: isize,
522        spread: isize,
523        blur: usize,
524        color: &crate::color::Color,
525    ) {
526        let sx = x + offset_x - spread;
527        let sy = y + offset_y - spread;
528        let sw = (w as isize + spread * 2).max(0) as f32;
529        let sh = (h as isize + spread * 2).max(0) as f32;
530        let sr = (radius as f32 + spread as f32).max(0.0);
531        let blur_f = blur as f32;
532
533        let fx = sx as f32;
534        let fy = sy as f32;
535        let draw_x = (fx - blur_f).floor() as isize;
536        let draw_y = (fy - blur_f).floor() as isize;
537        let draw_max_x = (fx + sw + blur_f).ceil() as isize;
538        let draw_max_y = (fy + sh + blur_f).ceil() as isize;
539
540        for py in draw_y..draw_max_y {
541            if py < 0 || py >= self.height as isize {
542                continue;
543            }
544            for px in draw_x..draw_max_x {
545                if px < 0 || px >= self.width as isize {
546                    continue;
547                }
548
549                let pfx = px as f32 + 0.5;
550                let pfy = py as f32 + 0.5;
551                let dist = rounded_rect_sdf(pfx, pfy, fx, fy, sw, sh, sr);
552
553                if dist >= blur_f || dist < 0.0 {
554                    if dist < 0.0 {
555                        let alpha = color.a as f32 / 255.0;
556                        if alpha > 0.0 {
557                            self.blend_pixel(px, py, color, alpha);
558                        }
559                    }
560                    continue;
561                }
562
563                let alpha = (color.a as f32 / 255.0) * (1.0 - dist / blur_f);
564                if alpha > 0.0 {
565                    self.blend_pixel(px, py, color, alpha);
566                }
567            }
568        }
569    }
570
571    // ─── Bezier Curves ──────────────────────────────────────────────────
572
573    /// Draws a quadratic bezier curve from p0 to p2 with control point p1
574    pub fn draw_bezier_quad(
575        &mut self,
576        x0: f32,
577        y0: f32,
578        x1: f32,
579        y1: f32,
580        x2: f32,
581        y2: f32,
582        th: usize,
583        color: &crate::color::Color,
584    ) {
585        let chord = ((x2 - x0).powi(2) + (y2 - y0).powi(2)).sqrt();
586        let control_net = ((x1 - x0).powi(2) + (y1 - y0).powi(2)).sqrt()
587            + ((x2 - x1).powi(2) + (y2 - y1).powi(2)).sqrt();
588        let steps = ((chord + control_net) * 2.0).ceil() as usize;
589        let steps = steps.max(32);
590
591        let mut prev_x = x0;
592        let mut prev_y = y0;
593
594        for i in 1..=steps {
595            let t = i as f32 / steps as f32;
596            let inv = 1.0 - t;
597            let cur_x = inv * inv * x0 + 2.0 * inv * t * x1 + t * t * x2;
598            let cur_y = inv * inv * y0 + 2.0 * inv * t * y1 + t * t * y2;
599
600            self.draw_line_aa_impl(prev_x, prev_y, cur_x, cur_y, th as f32, color);
601
602            prev_x = cur_x;
603            prev_y = cur_y;
604        }
605    }
606
607    /// Draws a cubic bezier curve from p0 to p3 with control points p1 and p2
608    pub fn draw_bezier_cubic(
609        &mut self,
610        x0: f32,
611        y0: f32,
612        x1: f32,
613        y1: f32,
614        x2: f32,
615        y2: f32,
616        x3: f32,
617        y3: f32,
618        th: usize,
619        color: &crate::color::Color,
620    ) {
621        let chord = ((x3 - x0).powi(2) + (y3 - y0).powi(2)).sqrt();
622        let control_net = ((x1 - x0).powi(2) + (y1 - y0).powi(2)).sqrt()
623            + ((x2 - x1).powi(2) + (y2 - y1).powi(2)).sqrt()
624            + ((x3 - x2).powi(2) + (y3 - y2).powi(2)).sqrt();
625        let steps = ((chord + control_net) * 2.0).ceil() as usize;
626        let steps = steps.max(32);
627
628        let mut prev_x = x0;
629        let mut prev_y = y0;
630
631        for i in 1..=steps {
632            let t = i as f32 / steps as f32;
633            let inv = 1.0 - t;
634            let cur_x = inv * inv * inv * x0
635                + 3.0 * inv * inv * t * x1
636                + 3.0 * inv * t * t * x2
637                + t * t * t * x3;
638            let cur_y = inv * inv * inv * y0
639                + 3.0 * inv * inv * t * y1
640                + 3.0 * inv * t * t * y2
641                + t * t * t * y3;
642
643            self.draw_line_aa_impl(prev_x, prev_y, cur_x, cur_y, th as f32, color);
644
645            prev_x = cur_x;
646            prev_y = cur_y;
647        }
648    }
649
650    // ─── Text ───────────────────────────────────────────────────────────
651
652    /// Draws text using the passed font
653    pub fn draw_text(&mut self, x: usize, y: usize, text: &crate::ui::text::Text, size: f32, color: &crate::color::Color) {
654        use fontdue::layout::{Layout, LayoutSettings, CoordinateSystem, TextStyle};
655
656        let fg_r = color.r as u32;
657        let fg_g = color.g as u32;
658        let fg_b = color.b as u32;
659
660        let fonts = text.font.as_slice();
661
662        let mut layout = Layout::new(CoordinateSystem::PositiveYDown);
663        layout.reset(&LayoutSettings {
664            x: x as f32,
665            y: y as f32,
666            ..Default::default()
667        });
668
669        layout.append(&fonts, &TextStyle::new(&text.text, size, 0));
670
671        for glyph in layout.glyphs() {
672            let (metrics, bitmap) = text.font.font.rasterize_config(glyph.key);
673
674            let glyph_x = glyph.x as i32;
675            let glyph_y = glyph.y as i32;
676
677            for row in 0..metrics.height {
678                for col in 0..metrics.width {
679                    let px = glyph_x + col as i32;
680                    let py = glyph_y + row as i32;
681
682                    if px < 0 || py < 0 { continue; }
683                    let (px, py) = (px as usize, py as usize);
684                    if px >= self.width || py >= self.height { continue; }
685
686                    let alpha = bitmap[row * metrics.width + col] as u32;
687                    if alpha == 0 { continue; }
688
689                    let idx = py * self.width + px;
690                    let bg = self.framebuffer_raw[idx];
691                    let bg_r = (bg >> 16) & 0xFF;
692                    let bg_g = (bg >> 8)  & 0xFF;
693                    let bg_b =  bg        & 0xFF;
694
695                    let r = (fg_r * alpha + bg_r * (255 - alpha)) / 255;
696                    let g = (fg_g * alpha + bg_g * (255 - alpha)) / 255;
697                    let b = (fg_b * alpha + bg_b * (255 - alpha)) / 255;
698
699                    self.framebuffer_raw[idx] = (r << 16) | (g << 8) | b;
700                }
701            }
702        }
703    }
704
705    // ─── Window Management ──────────────────────────────────────────────
706
707    /// Updates the window. Should be called in a loop until the window is to be closed
708    pub fn update(&mut self) {
709        self.window
710            .update_with_buffer(self.framebuffer_raw.as_slice(), self.width, self.height)
711            .unwrap();
712    }
713
714    pub fn get_mouse_state(&self) -> MouseState {
715        let pos = self.window.get_mouse_pos(minifb::MouseMode::Clamp).unwrap();
716        let lmb = self.window.get_mouse_down(minifb::MouseButton::Left);
717        let rmb = self.window.get_mouse_down(minifb::MouseButton::Right);
718        MouseState {
719            pos_x: pos.0,
720            pos_y: pos.1,
721            rmb_clicked: rmb,
722            lmb_clicked: lmb,
723        }
724    }
725
726    // ─── Text Centering ─────────────────────────────────────────────────
727
728    /// Draws text centered both horizontally and vertically within a bounding box
729    pub fn draw_text_centered(
730        &mut self,
731        x: usize,
732        y: usize,
733        w: usize,
734        h: usize,
735        text: &crate::ui::text::Text,
736        size: f32,
737        color: &crate::color::Color,
738    ) {
739        let text_w = text.get_width_precise(size);
740        let lm = text.font.font.horizontal_line_metrics(size).unwrap();
741        let tx = (x as f32 + (w as f32 / 2.0) - (text_w / 2.0)).max(0.0) as usize;
742        let ty = (y as f32 + (h as f32 / 2.0) - (lm.ascent / 2.0) + (lm.descent / 3.0)).max(0.0) as usize;
743        self.draw_text(tx, ty, text, size, color);
744    }
745
746    // ─── Blur ───────────────────────────────────────────────────────────
747
748    /// Applies a separable box blur to a rectangular region of the framebuffer.
749    /// Uses an O(n) sliding window — performance is independent of blur radius.
750    pub fn blur_region(&mut self, rx: usize, ry: usize, rw: usize, rh: usize, radius: usize) {
751        if radius == 0 || rw == 0 || rh == 0 { return; }
752        let x0 = rx.min(self.width);
753        let y0 = ry.min(self.height);
754        let x1 = (rx + rw).min(self.width);
755        let y1 = (ry + rh).min(self.height);
756        let w = x1 - x0;
757        let h = y1 - y0;
758        if w == 0 || h == 0 { return; }
759
760        let r = radius as i32;
761        let diam = (2 * r + 1) as u32;
762        let buf = &mut self.framebuffer_raw;
763        let fb_w = self.width;
764        let mut temp = vec![0u32; w * h];
765
766        // Pass 1: horizontal — framebuffer → temp
767        for row in 0..h {
768            let (mut sr, mut sg, mut sb) = (0u32, 0u32, 0u32);
769            let buf_row = (y0 + row) * fb_w + x0;
770            for dx in -r..=r {
771                let sx = dx.clamp(0, w as i32 - 1) as usize;
772                let p = buf[buf_row + sx];
773                sr += (p >> 16) & 0xFF; sg += (p >> 8) & 0xFF; sb += p & 0xFF;
774            }
775            for col in 0..w {
776                temp[row * w + col] = ((sr / diam) << 16) | ((sg / diam) << 8) | (sb / diam);
777                let old = (col as i32 - r).clamp(0, w as i32 - 1) as usize;
778                let p = buf[buf_row + old];
779                sr -= (p >> 16) & 0xFF; sg -= (p >> 8) & 0xFF; sb -= p & 0xFF;
780                let new = (col as i32 + r + 1).clamp(0, w as i32 - 1) as usize;
781                let p = buf[buf_row + new];
782                sr += (p >> 16) & 0xFF; sg += (p >> 8) & 0xFF; sb += p & 0xFF;
783            }
784        }
785
786        // Pass 2: vertical — temp → framebuffer
787        for col in 0..w {
788            let (mut sr, mut sg, mut sb) = (0u32, 0u32, 0u32);
789            for dy in -r..=r {
790                let sy = dy.clamp(0, h as i32 - 1) as usize;
791                let p = temp[sy * w + col];
792                sr += (p >> 16) & 0xFF; sg += (p >> 8) & 0xFF; sb += p & 0xFF;
793            }
794            for row in 0..h {
795                buf[(y0 + row) * fb_w + x0 + col] = ((sr / diam) << 16) | ((sg / diam) << 8) | (sb / diam);
796                let old = (row as i32 - r).clamp(0, h as i32 - 1) as usize;
797                let p = temp[old * w + col];
798                sr -= (p >> 16) & 0xFF; sg -= (p >> 8) & 0xFF; sb -= p & 0xFF;
799                let new = (row as i32 + r + 1).clamp(0, h as i32 - 1) as usize;
800                let p = temp[new * w + col];
801                sr += (p >> 16) & 0xFF; sg += (p >> 8) & 0xFF; sb += p & 0xFF;
802            }
803        }
804    }
805
806    /// Applies a box blur to a region, preserving pixels outside rounded corners.
807    /// This prevents the rectangular blur from leaking past rounded card edges.
808    pub fn blur_region_rounded(
809        &mut self,
810        rx: usize, ry: usize, rw: usize, rh: usize,
811        corner_radius: usize, blur_radius: usize,
812    ) {
813        if blur_radius == 0 || rw == 0 || rh == 0 { return; }
814        let r2 = (corner_radius * corner_radius) as isize;
815        let corners = [
816            (rx, ry, rx + corner_radius, ry + corner_radius, rx + corner_radius, ry + corner_radius),
817            (rx + rw - corner_radius, ry, rx + rw, ry + corner_radius, rx + rw - corner_radius, ry + corner_radius),
818            (rx, ry + rh - corner_radius, rx + corner_radius, ry + rh, rx + corner_radius, ry + rh - corner_radius),
819            (rx + rw - corner_radius, ry + rh - corner_radius, rx + rw, ry + rh, rx + rw - corner_radius, ry + rh - corner_radius),
820        ];
821
822        // Save corner pixels outside the rounded curve
823        let mut saved: Vec<(usize, u32)> = Vec::new();
824        for &(x0, y0, x1, y1, cx, cy) in &corners {
825            for py in y0..y1 {
826                for px in x0..x1 {
827                    let dx = px as isize - cx as isize;
828                    let dy = py as isize - cy as isize;
829                    if dx * dx + dy * dy > r2 {
830                        let idx = py * self.width + px;
831                        if idx < self.framebuffer_raw.len() {
832                            saved.push((idx, self.framebuffer_raw[idx]));
833                        }
834                    }
835                }
836            }
837        }
838
839        self.blur_region(rx, ry, rw, rh, blur_radius);
840
841        // Restore saved corner pixels
842        for &(idx, val) in &saved {
843            if idx < self.framebuffer_raw.len() {
844                self.framebuffer_raw[idx] = val;
845            }
846        }
847    }
848
849    // ─── Clipping ────────────────────────────────────────────────────────
850
851    /// Pushes a clipping rectangle. Drawing outside this region is discarded.
852    /// Nested clips are intersected.
853    pub fn push_clip(&mut self, x: usize, y: usize, w: usize, h: usize) {
854        let (cx, cy, cw, ch) = if let Some(&(px, py, pw, ph)) = self.clip_stack.last() {
855            let x0 = x.max(px);
856            let y0 = y.max(py);
857            let x1 = (x + w).min(px + pw);
858            let y1 = (y + h).min(py + ph);
859            if x1 > x0 && y1 > y0 {
860                (x0, y0, x1 - x0, y1 - y0)
861            } else {
862                (x, y, 0, 0)
863            }
864        } else {
865            (x, y, w, h)
866        };
867        self.clip_stack.push((cx, cy, cw, ch));
868    }
869
870    /// Pops the most recent clipping rectangle
871    pub fn pop_clip(&mut self) {
872        self.clip_stack.pop();
873    }
874
875    /// Returns the current clip bounds, or None if no clip is active
876    pub fn current_clip(&self) -> Option<(usize, usize, usize, usize)> {
877        self.clip_stack.last().copied()
878    }
879
880    /// Tests whether a pixel is inside the current clip region
881    #[inline]
882    fn clip_test(&self, x: usize, y: usize) -> bool {
883        if let Some(&(cx, cy, cw, ch)) = self.clip_stack.last() {
884            x >= cx && x < cx + cw && y >= cy && y < cy + ch
885        } else {
886            true
887        }
888    }
889
890    // ─── Text Wrapping ──────────────────────────────────────────────────
891
892    /// Draws text with word wrapping within `max_width` pixels.
893    /// Returns the total height used.
894    pub fn draw_text_wrapped(
895        &mut self,
896        x: usize,
897        y: usize,
898        text: &crate::ui::text::Text,
899        size: f32,
900        color: &crate::color::Color,
901        max_width: usize,
902    ) -> usize {
903        use fontdue::layout::{Layout, LayoutSettings, CoordinateSystem, TextStyle};
904
905        let lm = text.font.font.horizontal_line_metrics(size).unwrap();
906        let line_height = (lm.ascent - lm.descent + lm.line_gap).ceil() as usize;
907
908        let words: Vec<&str> = text.text.split_whitespace().collect();
909        if words.is_empty() {
910            return 0;
911        }
912
913        let fonts = text.font.as_slice();
914        let mut lines: Vec<String> = Vec::new();
915        let mut current_line = String::new();
916
917        for word in &words {
918            let test = if current_line.is_empty() {
919                word.to_string()
920            } else {
921                format!("{} {}", current_line, word)
922            };
923
924            // Measure the test line
925            let mut layout = Layout::new(CoordinateSystem::PositiveYDown);
926            layout.reset(&LayoutSettings::default());
927            layout.append(&fonts, &TextStyle::new(&test, size, 0));
928            let test_width = layout.glyphs().last().map_or(0.0, |g| {
929                g.x + text.font.font.metrics(g.parent, size).advance_width
930            });
931
932            if test_width <= max_width as f32 || current_line.is_empty() {
933                current_line = test;
934            } else {
935                lines.push(current_line);
936                current_line = word.to_string();
937            }
938        }
939        if !current_line.is_empty() {
940            lines.push(current_line);
941        }
942
943        let total_height = lines.len() * line_height;
944        for (i, line) in lines.iter().enumerate() {
945            let line_text = crate::ui::text::Text::new(line, text.font.clone());
946            self.draw_text(x, y + i * line_height, &line_text, size, color);
947        }
948
949        total_height
950    }
951
952
953    // ─── Private: AA Implementations ────────────────────────────────────
954
955    #[inline]
956    fn blend_pixel(&mut self, x: isize, y: isize, color: &crate::color::Color, alpha: f32) {
957        if x < 0 || y < 0 || x >= self.width as isize || y >= self.height as isize {
958            return;
959        }
960        if !self.clip_test(x as usize, y as usize) {
961            return;
962        }
963        let a = (alpha * 255.0) as u32;
964        if a == 0 {
965            return;
966        }
967        let idx = y as usize * self.width + x as usize;
968        if a >= 255 {
969            self.framebuffer_raw[idx] = color.as_u32();
970            return;
971        }
972        let bg = self.framebuffer_raw[idx];
973        let bg_r = (bg >> 16) & 0xFF;
974        let bg_g = (bg >> 8) & 0xFF;
975        let bg_b = bg & 0xFF;
976        let inv = 255 - a;
977        let r = (color.r as u32 * a + bg_r * inv) / 255;
978        let g = (color.g as u32 * a + bg_g * inv) / 255;
979        let b = (color.b as u32 * a + bg_b * inv) / 255;
980        self.framebuffer_raw[idx] = (r << 16) | (g << 8) | b;
981    }
982
983    #[inline]
984    fn set_pixel_safe(&mut self, x: isize, y: isize, value: u32) {
985        if x >= 0 && x < self.width as isize && y >= 0 && y < self.height as isize {
986            self.framebuffer_raw[y as usize * self.width + x as usize] = value;
987        }
988    }
989
990    /// Line with AA: uses Wu's algorithm for thin lines, SDF for thick
991    fn draw_line_aa_impl(&mut self, x0: f32, y0: f32, x1: f32, y1: f32, thickness: f32, color: &crate::color::Color) {
992        if thickness <= 1.5 && self.aa > 0.0 {
993            self.draw_line_wu(x0, y0, x1, y1, color);
994        } else if self.aa > 0.0 {
995            self.draw_line_thick_sdf(x0, y0, x1, y1, thickness, color);
996        } else {
997            self.draw_line_aliased(x0 as isize, y0 as isize, x1 as isize, y1 as isize, thickness as usize, color);
998        }
999    }
1000
1001    /// Xiaolin Wu's line algorithm
1002    fn draw_line_wu(&mut self, mut x0: f32, mut y0: f32, mut x1: f32, mut y1: f32, color: &crate::color::Color) {
1003        let steep = (y1 - y0).abs() > (x1 - x0).abs();
1004        if steep {
1005            std::mem::swap(&mut x0, &mut y0);
1006            std::mem::swap(&mut x1, &mut y1);
1007        }
1008        if x0 > x1 {
1009            std::mem::swap(&mut x0, &mut x1);
1010            std::mem::swap(&mut y0, &mut y1);
1011        }
1012
1013        let dx = x1 - x0;
1014        let dy = y1 - y0;
1015        let gradient = if dx < 0.001 { 1.0 } else { dy / dx };
1016
1017        let xend = x0.round();
1018        let yend = y0 + gradient * (xend - x0);
1019        let xgap = 1.0 - (x0 + 0.5).fract();
1020        let xpxl1 = xend as isize;
1021        let ypxl1 = yend.floor() as isize;
1022        if steep {
1023            self.blend_pixel(ypxl1, xpxl1, color, (1.0 - yend.fract()) * xgap);
1024            self.blend_pixel(ypxl1 + 1, xpxl1, color, yend.fract() * xgap);
1025        } else {
1026            self.blend_pixel(xpxl1, ypxl1, color, (1.0 - yend.fract()) * xgap);
1027            self.blend_pixel(xpxl1, ypxl1 + 1, color, yend.fract() * xgap);
1028        }
1029        let mut intery = yend + gradient;
1030
1031        let xend2 = x1.round();
1032        let yend2 = y1 + gradient * (xend2 - x1);
1033        let xgap2 = (x1 + 0.5).fract();
1034        let xpxl2 = xend2 as isize;
1035        let ypxl2 = yend2.floor() as isize;
1036        if steep {
1037            self.blend_pixel(ypxl2, xpxl2, color, (1.0 - yend2.fract()) * xgap2);
1038            self.blend_pixel(ypxl2 + 1, xpxl2, color, yend2.fract() * xgap2);
1039        } else {
1040            self.blend_pixel(xpxl2, ypxl2, color, (1.0 - yend2.fract()) * xgap2);
1041            self.blend_pixel(xpxl2, ypxl2 + 1, color, yend2.fract() * xgap2);
1042        }
1043
1044        for x in (xpxl1 + 1)..xpxl2 {
1045            let ipart = intery.floor() as isize;
1046            let fpart = intery.fract();
1047            if steep {
1048                self.blend_pixel(ipart, x, color, 1.0 - fpart);
1049                self.blend_pixel(ipart + 1, x, color, fpart);
1050            } else {
1051                self.blend_pixel(x, ipart, color, 1.0 - fpart);
1052                self.blend_pixel(x, ipart + 1, color, fpart);
1053            }
1054            intery += gradient;
1055        }
1056    }
1057
1058    /// Thick line using distance-to-segment SDF
1059    fn draw_line_thick_sdf(&mut self, x0: f32, y0: f32, x1: f32, y1: f32, thickness: f32, color: &crate::color::Color) {
1060        let dx = x1 - x0;
1061        let dy = y1 - y0;
1062        let len_sq = dx * dx + dy * dy;
1063        if len_sq < 0.001 {
1064            return;
1065        }
1066        let half = thickness / 2.0;
1067        let aa = self.aa;
1068
1069        let min_x = (x0.min(x1) - half - aa).floor() as isize;
1070        let max_x = (x0.max(x1) + half + aa).ceil() as isize;
1071        let min_y = (y0.min(y1) - half - aa).floor() as isize;
1072        let max_y = (y0.max(y1) + half + aa).ceil() as isize;
1073
1074        let min_x = min_x.max(0);
1075        let max_x = max_x.min(self.width as isize - 1);
1076        let min_y = min_y.max(0);
1077        let max_y = max_y.min(self.height as isize - 1);
1078
1079        for py in min_y..=max_y {
1080            for px in min_x..=max_x {
1081                let fx = px as f32 + 0.5;
1082                let fy = py as f32 + 0.5;
1083
1084                let t = ((fx - x0) * dx + (fy - y0) * dy) / len_sq;
1085                let t = t.clamp(0.0, 1.0);
1086                let closest_x = x0 + t * dx;
1087                let closest_y = y0 + t * dy;
1088                let dist = ((fx - closest_x).powi(2) + (fy - closest_y).powi(2)).sqrt();
1089
1090                let coverage = ((half + aa * 0.5 - dist) / aa).clamp(0.0, 1.0);
1091                if coverage > 0.0 {
1092                    self.blend_pixel(px, py, color, coverage);
1093                }
1094            }
1095        }
1096    }
1097
1098    /// Bresenham line (no AA)
1099    fn draw_line_aliased(&mut self, x0: isize, y0: isize, x1: isize, y1: isize, th: usize, color: &crate::color::Color) {
1100        let dx = (x1 - x0).abs();
1101        let dy = (y1 - y0).abs();
1102        let sx = if x0 < x1 { 1isize } else { -1 };
1103        let sy = if y0 < y1 { 1isize } else { -1 };
1104        let value = color.as_u32();
1105        let half = (th / 2) as isize;
1106
1107        let mut x = x0;
1108        let mut y = y0;
1109        let mut err = dx - dy;
1110
1111        loop {
1112            for oy in -half..=half {
1113                for ox in -half..=half {
1114                    let px = x + ox;
1115                    let py = y + oy;
1116                    if px >= 0 && px < self.width as isize && py >= 0 && py < self.height as isize {
1117                        self.framebuffer_raw[py as usize * self.width + px as usize] = value;
1118                    }
1119                }
1120            }
1121
1122            if x == x1 && y == y1 { break; }
1123
1124            let e2 = err * 2;
1125            if e2 > -dy { err -= dy; x += sx; }
1126            if e2 <  dx { err += dx; y += sy; }
1127        }
1128    }
1129
1130    /// Filled circle with SDF AA
1131    fn draw_circle_f_sdf(&mut self, cx: f32, cy: f32, radius: f32, color: &crate::color::Color, base_alpha: f32) {
1132        let aa = self.aa;
1133        let min_x = ((cx - radius - aa).floor() as isize).max(0);
1134        let max_x = ((cx + radius + aa).ceil() as isize).min(self.width as isize - 1);
1135        let min_y = ((cy - radius - aa).floor() as isize).max(0);
1136        let max_y = ((cy + radius + aa).ceil() as isize).min(self.height as isize - 1);
1137
1138        for py in min_y..=max_y {
1139            for px in min_x..=max_x {
1140                let fx = px as f32 + 0.5;
1141                let fy = py as f32 + 0.5;
1142                let dist = ((fx - cx).powi(2) + (fy - cy).powi(2)).sqrt() - radius;
1143                let coverage = ((aa * 0.5 - dist) / aa).clamp(0.0, 1.0);
1144                let final_alpha = coverage * base_alpha;
1145                if final_alpha > 0.0 {
1146                    self.blend_pixel(px, py, color, final_alpha);
1147                }
1148            }
1149        }
1150    }
1151
1152    /// Hollow circle (ring) with SDF AA
1153    fn draw_circle_sdf(&mut self, cx: f32, cy: f32, radius: f32, thickness: f32, color: &crate::color::Color) {
1154        let aa = self.aa;
1155        let half = thickness / 2.0;
1156        let min_x = ((cx - radius - half - aa).floor() as isize).max(0);
1157        let max_x = ((cx + radius + half + aa).ceil() as isize).min(self.width as isize - 1);
1158        let min_y = ((cy - radius - half - aa).floor() as isize).max(0);
1159        let max_y = ((cy + radius + half + aa).ceil() as isize).min(self.height as isize - 1);
1160
1161        for py in min_y..=max_y {
1162            for px in min_x..=max_x {
1163                let fx = px as f32 + 0.5;
1164                let fy = py as f32 + 0.5;
1165                let dist_from_center = ((fx - cx).powi(2) + (fy - cy).powi(2)).sqrt();
1166                let dist = (dist_from_center - radius).abs() - half;
1167                let coverage = ((aa * 0.5 - dist) / aa).clamp(0.0, 1.0);
1168                if coverage > 0.0 {
1169                    self.blend_pixel(px, py, color, coverage);
1170                }
1171            }
1172        }
1173    }
1174
1175    /// Filled circle without AA
1176    fn draw_circle_f_aliased(&mut self, cx: isize, cy: isize, radius: usize, color: &crate::color::Color) {
1177        let r = radius as isize;
1178        let value = color.as_u32();
1179        for dy in -r..=r {
1180            let half_w = isqrt_i(r * r - dy * dy);
1181            let py = cy + dy;
1182            if py < 0 || py >= self.height as isize { continue; }
1183            let start_x = (cx - half_w).max(0) as usize;
1184            let end_x = ((cx + half_w + 1) as usize).min(self.width);
1185            if start_x >= end_x { continue; }
1186            let row = py as usize * self.width;
1187            self.framebuffer_raw[row + start_x..row + end_x].fill(value);
1188        }
1189    }
1190
1191    /// Hollow circle without AA (midpoint algorithm)
1192    fn draw_circle_aliased(&mut self, cx: isize, cy: isize, radius: usize, color: &crate::color::Color) {
1193        let value = color.as_u32();
1194        let mut x = radius as isize;
1195        let mut y: isize = 0;
1196        let mut err: isize = 1 - x;
1197
1198        while x >= y {
1199            self.set_pixel_safe(cx + x, cy + y, value);
1200            self.set_pixel_safe(cx - x, cy + y, value);
1201            self.set_pixel_safe(cx + x, cy - y, value);
1202            self.set_pixel_safe(cx - x, cy - y, value);
1203            self.set_pixel_safe(cx + y, cy + x, value);
1204            self.set_pixel_safe(cx - y, cy + x, value);
1205            self.set_pixel_safe(cx + y, cy - x, value);
1206            self.set_pixel_safe(cx - y, cy - x, value);
1207
1208            y += 1;
1209            if err < 0 {
1210                err += 2 * y + 1;
1211            } else {
1212                x -= 1;
1213                err += 2 * (y - x) + 1;
1214            }
1215        }
1216    }
1217
1218    /// Filled ellipse with SDF AA
1219    fn draw_ellipse_f_sdf(&mut self, cx: f32, cy: f32, rx: f32, ry: f32, color: &crate::color::Color, base_alpha: f32) {
1220        let aa = self.aa;
1221        let min_x = ((cx - rx - aa).floor() as isize).max(0);
1222        let max_x = ((cx + rx + aa).ceil() as isize).min(self.width as isize - 1);
1223        let min_y = ((cy - ry - aa).floor() as isize).max(0);
1224        let max_y = ((cy + ry + aa).ceil() as isize).min(self.height as isize - 1);
1225
1226        for py in min_y..=max_y {
1227            for px in min_x..=max_x {
1228                let fx = px as f32 + 0.5;
1229                let fy = py as f32 + 0.5;
1230                let nx = (fx - cx) / rx;
1231                let ny = (fy - cy) / ry;
1232                let d = (nx * nx + ny * ny).sqrt();
1233                let r_min = rx.min(ry);
1234                let dist = (d - 1.0) * r_min;
1235                let coverage = ((aa * 0.5 - dist) / aa).clamp(0.0, 1.0);
1236                let final_alpha = coverage * base_alpha;
1237                if final_alpha > 0.0 {
1238                    self.blend_pixel(px, py, color, final_alpha);
1239                }
1240            }
1241        }
1242    }
1243
1244    /// Hollow ellipse with SDF AA
1245    fn draw_ellipse_sdf(&mut self, cx: f32, cy: f32, rx: f32, ry: f32, thickness: f32, color: &crate::color::Color) {
1246        let aa = self.aa;
1247        let half = thickness / 2.0;
1248        let min_x = ((cx - rx - half - aa).floor() as isize).max(0);
1249        let max_x = ((cx + rx + half + aa).ceil() as isize).min(self.width as isize - 1);
1250        let min_y = ((cy - ry - half - aa).floor() as isize).max(0);
1251        let max_y = ((cy + ry + half + aa).ceil() as isize).min(self.height as isize - 1);
1252
1253        for py in min_y..=max_y {
1254            for px in min_x..=max_x {
1255                let fx = px as f32 + 0.5;
1256                let fy = py as f32 + 0.5;
1257                let nx = (fx - cx) / rx;
1258                let ny = (fy - cy) / ry;
1259                let d = (nx * nx + ny * ny).sqrt();
1260                let r_min = rx.min(ry);
1261                let dist = ((d - 1.0) * r_min).abs() - half;
1262                let coverage = ((aa * 0.5 - dist) / aa).clamp(0.0, 1.0);
1263                if coverage > 0.0 {
1264                    self.blend_pixel(px, py, color, coverage);
1265                }
1266            }
1267        }
1268    }
1269
1270    /// Filled ellipse without AA
1271    fn draw_ellipse_f_aliased(&mut self, cx: isize, cy: isize, rx: usize, ry: usize, color: &crate::color::Color) {
1272        let value = color.as_u32();
1273        let rx_i = rx as isize;
1274        let ry_i = ry as isize;
1275        for dy in -ry_i..=ry_i {
1276            let py = cy + dy;
1277            if py < 0 || py >= self.height as isize { continue; }
1278            let half_w = (rx_i as f32 * (1.0 - (dy * dy) as f32 / (ry_i * ry_i) as f32).sqrt()) as isize;
1279            let start_x = (cx - half_w).max(0) as usize;
1280            let end_x = ((cx + half_w + 1) as usize).min(self.width);
1281            if start_x >= end_x { continue; }
1282            let row = py as usize * self.width;
1283            self.framebuffer_raw[row + start_x..row + end_x].fill(value);
1284        }
1285    }
1286
1287    /// Hollow ellipse without AA (parametric)
1288    fn draw_ellipse_aliased(&mut self, cx: isize, cy: isize, rx: usize, ry: usize, color: &crate::color::Color) {
1289        let value = color.as_u32();
1290        let a = rx as f32;
1291        let b = ry as f32;
1292        let steps = ((rx + ry) * 4).max(64);
1293        let step_angle = std::f32::consts::TAU / steps as f32;
1294
1295        for i in 0..steps {
1296            let angle = i as f32 * step_angle;
1297            let px = (cx as f32 + a * angle.cos()) as isize;
1298            let py = (cy as f32 + b * angle.sin()) as isize;
1299            self.set_pixel_safe(px, py, value);
1300        }
1301    }
1302
1303    /// Filled rounded rect with SDF AA, respecting base_alpha
1304    fn draw_rounded_rect_f_sdf(&mut self, x: f32, y: f32, w: f32, h: f32, radius: f32, color: &crate::color::Color, base_alpha: f32) {
1305        let r = radius.min(w / 2.0).min(h / 2.0);
1306        let aa = self.aa;
1307        let min_px = ((x - aa).floor() as isize).max(0);
1308        let max_px = ((x + w + aa).ceil() as isize).min(self.width as isize - 1);
1309        let min_py = ((y - aa).floor() as isize).max(0);
1310        let max_py = ((y + h + aa).ceil() as isize).min(self.height as isize - 1);
1311
1312        let value = color.as_u32();
1313
1314        for py in min_py..=max_py {
1315            for px in min_px..=max_px {
1316                let pfx = px as f32 + 0.5;
1317                let pfy = py as f32 + 0.5;
1318                let dist = rounded_rect_sdf(pfx, pfy, x, y, w, h, r);
1319                let coverage = ((aa * 0.5 - dist) / aa).clamp(0.0, 1.0);
1320                let final_alpha = coverage * base_alpha;
1321                if final_alpha >= 1.0 {
1322                    self.framebuffer_raw[py as usize * self.width + px as usize] = value;
1323                } else if final_alpha > 0.0 {
1324                    self.blend_pixel(px, py, color, final_alpha);
1325                }
1326            }
1327        }
1328    }
1329
1330    /// Hollow rounded rect with SDF AA
1331    fn draw_rounded_rect_sdf(&mut self, x: f32, y: f32, w: f32, h: f32, radius: f32, thickness: f32, color: &crate::color::Color) {
1332        let r = radius.min(w / 2.0).min(h / 2.0);
1333        let aa = self.aa;
1334        let half = thickness / 2.0;
1335        let min_px = ((x - half - aa).floor() as isize).max(0);
1336        let max_px = ((x + w + half + aa).ceil() as isize).min(self.width as isize - 1);
1337        let min_py = ((y - half - aa).floor() as isize).max(0);
1338        let max_py = ((y + h + half + aa).ceil() as isize).min(self.height as isize - 1);
1339
1340        for py in min_py..=max_py {
1341            for px in min_px..=max_px {
1342                let pfx = px as f32 + 0.5;
1343                let pfy = py as f32 + 0.5;
1344                let dist = rounded_rect_sdf(pfx, pfy, x, y, w, h, r).abs() - half;
1345                let coverage = ((aa * 0.5 - dist) / aa).clamp(0.0, 1.0);
1346                if coverage > 0.0 {
1347                    self.blend_pixel(px, py, color, coverage);
1348                }
1349            }
1350        }
1351    }
1352
1353    /// Filled rounded rect without AA
1354    fn draw_rounded_rect_f_aliased(&mut self, x: usize, y: usize, w: usize, h: usize, radius: usize, color: &crate::color::Color) {
1355        let r = radius.min(w / 2).min(h / 2);
1356        let value = color.as_u32();
1357
1358        for dy in 0..h {
1359            let (row_start, row_end) = if dy < r {
1360                let cy = r - dy;
1361                let dx = r - isqrt(r * r - cy * cy);
1362                (x + dx, x + w - dx)
1363            } else if dy >= h - r {
1364                let cy = dy - (h - 1 - r);
1365                let dx = r - isqrt(r * r - cy * cy);
1366                (x + dx, x + w - dx)
1367            } else {
1368                (x, x + w)
1369            };
1370
1371            let start = (y + dy) * self.width + row_start;
1372            let len = row_end - row_start;
1373            self.framebuffer_raw[start..start + len].fill(value);
1374        }
1375    }
1376
1377    /// Hollow rounded rect without AA
1378    fn draw_rounded_rect_aliased(&mut self, x: usize, y: usize, w: usize, h: usize, radius: usize, color: &crate::color::Color) {
1379        let r = radius.min(w / 2).min(h / 2);
1380        let value = color.as_u32();
1381
1382        for px in (x + r)..(x + w - r) {
1383            self.framebuffer_raw[y * self.width + px] = value;
1384            self.framebuffer_raw[(y + h - 1) * self.width + px] = value;
1385        }
1386        for py in (y + r)..(y + h - r) {
1387            self.framebuffer_raw[py * self.width + x] = value;
1388            self.framebuffer_raw[py * self.width + x + w - 1] = value;
1389        }
1390
1391        let cx_tl = x + r;
1392        let cy_tl = y + r;
1393        let cx_tr = x + w - 1 - r;
1394        let cy_tr = y + r;
1395        let cx_bl = x + r;
1396        let cy_bl = y + h - 1 - r;
1397        let cx_br = x + w - 1 - r;
1398        let cy_br = y + h - 1 - r;
1399
1400        let mut ix = r as isize;
1401        let mut iy: isize = 0;
1402        let mut err: isize = 1 - ix;
1403
1404        while ix >= iy {
1405            self.set_pixel_safe(cx_tl as isize - ix, cy_tl as isize - iy, value);
1406            self.set_pixel_safe(cx_tl as isize - iy, cy_tl as isize - ix, value);
1407            self.set_pixel_safe(cx_tr as isize + ix, cy_tr as isize - iy, value);
1408            self.set_pixel_safe(cx_tr as isize + iy, cy_tr as isize - ix, value);
1409            self.set_pixel_safe(cx_bl as isize - ix, cy_bl as isize + iy, value);
1410            self.set_pixel_safe(cx_bl as isize - iy, cy_bl as isize + ix, value);
1411            self.set_pixel_safe(cx_br as isize + ix, cy_br as isize + iy, value);
1412            self.set_pixel_safe(cx_br as isize + iy, cy_br as isize + ix, value);
1413
1414            iy += 1;
1415            if err < 0 {
1416                err += 2 * iy + 1;
1417            } else {
1418                ix -= 1;
1419                err += 2 * (iy - ix) + 1;
1420            }
1421        }
1422    }
1423}
1424
1425pub struct MouseState {
1426    pub pos_x: f32,
1427    pub pos_y: f32,
1428    pub rmb_clicked: bool,
1429    pub lmb_clicked: bool
1430}
1431
1432/// Integer square root (usize)
1433#[inline]
1434fn isqrt(n: usize) -> usize {
1435    (n as f32).sqrt() as usize
1436}
1437
1438/// Integer square root (isize)
1439#[inline]
1440fn isqrt_i(n: isize) -> isize {
1441    (n as f32).sqrt() as isize
1442}
1443
1444/// Signed distance field for a rounded rectangle.
1445/// Returns negative inside, positive outside, zero on the boundary.
1446#[inline]
1447fn rounded_rect_sdf(px: f32, py: f32, x: f32, y: f32, w: f32, h: f32, r: f32) -> f32 {
1448    let cx = x + w / 2.0;
1449    let cy = y + h / 2.0;
1450    let dx = (px - cx).abs() - (w / 2.0 - r);
1451    let dy = (py - cy).abs() - (h / 2.0 - r);
1452    let outside = (dx.max(0.0).powi(2) + dy.max(0.0).powi(2)).sqrt();
1453    let inside = dx.max(dy).min(0.0);
1454    outside + inside - r
1455}