Skip to main content

microui_redux/
graphics.rs

1//
2// Copyright 2022-Present (c) Raja Lehtihet & Wael El Oraiby
3//
4// Redistribution and use in source and binary forms, with or without
5// modification, are permitted provided that the following conditions are met:
6//
7// 1. Redistributions of source code must retain the above copyright notice,
8// this list of conditions and the following disclaimer.
9//
10// 2. Redistributions in binary form must reproduce the above copyright notice,
11// this list of conditions and the following disclaimer in the documentation
12// and/or other materials provided with the distribution.
13//
14// 3. Neither the name of the copyright holder nor the names of its contributors
15// may be used to endorse or promote products derived from this software without
16// specific prior written permission.
17//
18// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
22// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28// POSSIBILITY OF SUCH DAMAGE.
29//
30// -----------------------------------------------------------------------------
31//! Widget-local 2D geometry recording.
32//!
33//! The rest of the UI draw path is largely rectangle-oriented and mostly works in container
34//! coordinates. This module adds a dedicated widget-local geometry builder that:
35//! - accepts points relative to the current widget origin,
36//! - forwards widget-local clips onto the shared draw-context clip stack,
37//! - clips tessellated triangles against the current widget-local clip rect in software,
38//! - emits already-clipped retained triangle commands that do not depend on backend scissor state.
39//!
40//! Because clipping happens before commands are emitted, clip-stack changes no longer need to
41//! fragment the retained command stream. A single graphics closure can therefore accumulate one
42//! larger triangle batch even if it uses nested local clip scopes internally. The actual triangle
43//! vertices live in the container-owned arena held by `DrawCtx`, so individual widgets do not
44//! allocate their own per-batch vertex vectors.
45
46use crate::container::Command;
47use crate::draw_context::{control_text_position_with_font, intersect_clip_rect, DrawCtx};
48use crate::*;
49use std::rc::Rc;
50
51const GEOM_EPS: f32 = 1.0e-5;
52const GEOM_EPS_SQ: f32 = GEOM_EPS * GEOM_EPS;
53
54// Applies an integer translation without changing extents. Geometry code uses this to hop
55// between widget-local and screen-space rectangles while keeping clip math reusable.
56fn translate_rect(rect: Recti, offset: Vec2i) -> Recti {
57    Recti::new(rect.x + offset.x, rect.y + offset.y, rect.width, rect.height)
58}
59
60// Applies the widget's screen-space origin to one retained triangle vertex without disturbing its
61// UV or color payload.
62fn translate_vertex(vertex: Vertex, offset: Vec2f) -> Vertex {
63    Vertex::new(vertex.position() + offset, vertex.tex_coord(), vertex.color())
64}
65
66// Computes a conservative integer bounding box for a set of floating-point positions. The helper
67// is only used by tests to verify geometric translation behavior.
68#[cfg(test)]
69fn rect_from_points(points: &[Vec2f]) -> Recti {
70    let mut min_x = f32::INFINITY;
71    let mut min_y = f32::INFINITY;
72    let mut max_x = f32::NEG_INFINITY;
73    let mut max_y = f32::NEG_INFINITY;
74
75    for point in points {
76        min_x = min_x.min(point.x);
77        min_y = min_y.min(point.y);
78        max_x = max_x.max(point.x);
79        max_y = max_y.max(point.y);
80    }
81
82    let x0 = min_x.floor() as i32;
83    let y0 = min_y.floor() as i32;
84    let x1 = max_x.ceil() as i32;
85    let y1 = max_y.ceil() as i32;
86    Recti::new(x0, y0, (x1 - x0).max(0), (y1 - y0).max(0))
87}
88
89// Returns the signed 2D cross product. This is the core orientation predicate reused by the
90// polygon cleanup, convexity tests, and point-in-triangle checks.
91fn cross2(a: Vec2f, b: Vec2f) -> f32 {
92    a.x * b.y - a.y * b.x
93}
94
95// Uses squared distance so degenerate/duplicate vertices can be rejected without paying for a
96// square root in hot polygon preprocessing code.
97fn distance_sq(a: Vec2f, b: Vec2f) -> f32 {
98    let dx = a.x - b.x;
99    let dy = a.y - b.y;
100    dx * dx + dy * dy
101}
102
103// Computes polygon winding and area in one pass. The sign decides whether the input needs to be
104// reversed before triangulation, while near-zero area indicates a degenerate polygon.
105fn signed_area(points: &[Vec2f]) -> f32 {
106    if points.len() < 3 {
107        return 0.0;
108    }
109
110    let mut area = 0.0;
111    for idx in 0..points.len() {
112        let curr = points[idx];
113        let next = points[(idx + 1) % points.len()];
114        area += curr.x * next.y - next.x * curr.y;
115    }
116    area * 0.5
117}
118
119// Removes duplicate closing points, repeated neighbors, and strictly collinear vertices before
120// triangulation. The fill paths rely on a compact boundary because both the convex fast path and
121// ear clipping become simpler and faster once obvious redundancy is stripped out.
122fn dedupe_and_simplify_polygon(points: &[Vec2f]) -> Vec<Vec2f> {
123    let mut deduped = Vec::with_capacity(points.len());
124    for point in points {
125        if deduped.last().map(|prev| distance_sq(*prev, *point) > GEOM_EPS_SQ).unwrap_or(true) {
126            deduped.push(*point);
127        }
128    }
129
130    if deduped.len() > 1 && distance_sq(deduped[0], *deduped.last().unwrap()) <= GEOM_EPS_SQ {
131        deduped.pop();
132    }
133
134    if deduped.len() < 3 {
135        return Vec::new();
136    }
137
138    let mut simplified = Vec::with_capacity(deduped.len());
139    for idx in 0..deduped.len() {
140        let prev = deduped[(idx + deduped.len() - 1) % deduped.len()];
141        let curr = deduped[idx];
142        let next = deduped[(idx + 1) % deduped.len()];
143
144        if distance_sq(prev, curr) <= GEOM_EPS_SQ || distance_sq(curr, next) <= GEOM_EPS_SQ {
145            continue;
146        }
147
148        if cross2(curr - prev, next - curr).abs() <= GEOM_EPS {
149            continue;
150        }
151
152        simplified.push(curr);
153    }
154
155    simplified
156}
157
158// Ear clipping assumes counter-clockwise winding, so "convex" means a positive turn here.
159fn is_convex_ccw(prev: Vec2f, curr: Vec2f, next: Vec2f) -> bool {
160    cross2(curr - prev, next - curr) > GEOM_EPS
161}
162
163// Detects the cheap convex case up front so common polygons can skip the heavier ear-clipping
164// loop and fall straight into a triangle fan.
165fn is_convex_polygon_ccw(points: &[Vec2f]) -> bool {
166    if points.len() < 3 {
167        return false;
168    }
169
170    for idx in 0..points.len() {
171        let prev = points[(idx + points.len() - 1) % points.len()];
172        let curr = points[idx];
173        let next = points[(idx + 1) % points.len()];
174        if !is_convex_ccw(prev, curr, next) {
175            return false;
176        }
177    }
178    true
179}
180
181// Uses inclusive edge tests so boundary points still count as inside. That makes the ear test
182// robust against vertices that land directly on a candidate triangle edge after simplification.
183fn point_in_triangle_ccw(point: Vec2f, a: Vec2f, b: Vec2f, c: Vec2f) -> bool {
184    let ab = cross2(b - a, point - a);
185    let bc = cross2(c - b, point - b);
186    let ca = cross2(a - c, point - c);
187    ab >= -GEOM_EPS && bc >= -GEOM_EPS && ca >= -GEOM_EPS
188}
189
190#[derive(Copy, Clone)]
191enum RectClipEdge {
192    Left,
193    Right,
194    Top,
195    Bottom,
196}
197
198// Linear interpolation for positions/UVs used when a triangle edge intersects a clip boundary.
199// The clipping math works in floating point, so this keeps the generated vertices continuous
200// rather than snapping to integer pixels.
201fn lerp_vec2(a: Vec2f, b: Vec2f, t: f32) -> Vec2f {
202    let omt = 1.0 - t;
203    Vec2f::new(a.x * omt + b.x * t, a.y * omt + b.y * t)
204}
205
206// Vertex colors are stored as bytes, so interpolation happens in float and rounds back to `u8`
207// after clipping creates new boundary vertices.
208fn lerp_u8(a: u8, b: u8, t: f32) -> u8 {
209    ((a as f32) + (b as f32 - a as f32) * t).round().clamp(0.0, 255.0) as u8
210}
211
212// Interpolates RGBA channels component-wise so clipped triangles preserve gradients if triangle
213// vertices ever carry different colors in the future.
214fn lerp_color4b(a: Color4b, b: Color4b, t: f32) -> Color4b {
215    color4b(lerp_u8(a.x, b.x, t), lerp_u8(a.y, b.y, t), lerp_u8(a.z, b.z, t), lerp_u8(a.w, b.w, t))
216}
217
218// Builds a new vertex along an edge intersection while preserving the original attribute layout.
219// The clipping helpers operate on full `Vertex` values so the same code can be reused by both the
220// widget-local graphics builder and the canvas replay path.
221fn lerp_vertex(a: Vertex, b: Vertex, t: f32) -> Vertex {
222    Vertex::new(
223        lerp_vec2(a.position(), b.position(), t),
224        lerp_vec2(a.tex_coord(), b.tex_coord(), t),
225        lerp_color4b(a.color(), b.color(), t),
226    )
227}
228
229// Tests whether a point lies inside one half-plane of the axis-aligned clip rectangle. Inclusive
230// comparisons keep boundary-aligned edges stable and avoid dropping triangles that exactly touch
231// the clip.
232fn point_inside_clip_edge(point: Vec2f, edge: RectClipEdge, clip: Recti) -> bool {
233    let left = clip.x as f32;
234    let right = (clip.x + clip.width) as f32;
235    let top = clip.y as f32;
236    let bottom = (clip.y + clip.height) as f32;
237    match edge {
238        RectClipEdge::Left => point.x >= left - GEOM_EPS,
239        RectClipEdge::Right => point.x <= right + GEOM_EPS,
240        RectClipEdge::Top => point.y >= top - GEOM_EPS,
241        RectClipEdge::Bottom => point.y <= bottom + GEOM_EPS,
242    }
243}
244
245// Computes the edge parameter for the intersection against one clip boundary. The result is
246// clamped to [0, 1] so numerical noise on nearly-parallel segments cannot generate runaway
247// interpolation values.
248fn intersection_t_for_edge(a: Vec2f, b: Vec2f, edge: RectClipEdge, clip: Recti) -> f32 {
249    let (start, delta, boundary) = match edge {
250        RectClipEdge::Left => (a.x, b.x - a.x, clip.x as f32),
251        RectClipEdge::Right => (a.x, b.x - a.x, (clip.x + clip.width) as f32),
252        RectClipEdge::Top => (a.y, b.y - a.y, clip.y as f32),
253        RectClipEdge::Bottom => (a.y, b.y - a.y, (clip.y + clip.height) as f32),
254    };
255
256    if delta.abs() <= GEOM_EPS {
257        0.0
258    } else {
259        ((boundary - start) / delta).clamp(0.0, 1.0)
260    }
261}
262
263// Creates the actual boundary vertex for one clipped segment. Attribute interpolation stays in one
264// place so both local and replay-time clipping reuse the same rules.
265fn intersect_vertex_edge(a: Vertex, b: Vertex, edge: RectClipEdge, clip: Recti) -> Vertex {
266    let t = intersection_t_for_edge(a.position(), b.position(), edge, clip);
267    lerp_vertex(a, b, t)
268}
269
270// Suppresses duplicate consecutive vertices that can appear when an input edge lies directly on a
271// clip boundary. This keeps the later triangle-fan reconstruction compact and avoids degenerate
272// zero-area triangles.
273fn push_unique_vertex(dst: &mut [Vertex; 8], count: &mut usize, vertex: Vertex) {
274    if *count > 0 && distance_sq(dst[*count - 1].position(), vertex.position()) <= GEOM_EPS_SQ {
275        dst[*count - 1] = vertex;
276        return;
277    }
278
279    debug_assert!(*count < dst.len(), "rect-clipped triangle exceeded fixed vertex budget");
280    dst[*count] = vertex;
281    *count += 1;
282}
283
284// Clips one polygon buffer against one clip edge using Sutherland-Hodgman. The triangle path uses
285// fixed-size arrays because a triangle clipped by a rectangle yields at most seven vertices.
286fn clip_polygon_against_edge(input: &[Vertex; 8], input_count: usize, edge: RectClipEdge, clip: Recti, output: &mut [Vertex; 8]) -> usize {
287    if input_count == 0 {
288        return 0;
289    }
290
291    let mut out_count = 0;
292    let mut prev = input[input_count - 1];
293    let mut prev_inside = point_inside_clip_edge(prev.position(), edge, clip);
294
295    for curr in input.iter().copied().take(input_count) {
296        let curr_inside = point_inside_clip_edge(curr.position(), edge, clip);
297
298        if curr_inside != prev_inside {
299            let intersection = intersect_vertex_edge(prev, curr, edge, clip);
300            push_unique_vertex(output, &mut out_count, intersection);
301        }
302        if curr_inside {
303            push_unique_vertex(output, &mut out_count, curr);
304        }
305
306        prev = curr;
307        prev_inside = curr_inside;
308    }
309
310    if out_count > 1 && distance_sq(output[0].position(), output[out_count - 1].position()) <= GEOM_EPS_SQ {
311        out_count -= 1;
312    }
313
314    out_count
315}
316
317// Computes polygon area directly from vertex positions so clipped polygons can reject degenerate
318// cases before they are triangulated back into a fan.
319fn signed_area_vertices(points: &[Vertex]) -> f32 {
320    if points.len() < 3 {
321        return 0.0;
322    }
323
324    let mut area = 0.0;
325    for idx in 0..points.len() {
326        let curr = points[idx].position();
327        let next = points[(idx + 1) % points.len()].position();
328        area += curr.x * next.y - next.x * curr.y;
329    }
330    area * 0.5
331}
332
333/// Clips one triangle against `clip` and emits zero or more fully clipped triangles.
334///
335/// This is the shared software-clipper for retained widget-local geometry and replay-time canvas
336/// clipping. Working on full vertices instead of bare positions keeps interpolation logic in one
337/// place and makes future textured/gradient triangles possible without changing the API surface.
338pub(crate) fn clip_triangle_vertices_to_rect<F>(v0: Vertex, v1: Vertex, v2: Vertex, clip: Recti, mut emit: F)
339where
340    F: FnMut(Vertex, Vertex, Vertex),
341{
342    if clip.width <= 0 || clip.height <= 0 {
343        return;
344    }
345
346    let mut input = [Vertex::default(); 8];
347    let mut output = [Vertex::default(); 8];
348    input[0] = v0;
349    input[1] = v1;
350    input[2] = v2;
351    let mut input_count = 3usize;
352
353    for edge in [RectClipEdge::Left, RectClipEdge::Right, RectClipEdge::Top, RectClipEdge::Bottom] {
354        let output_count = clip_polygon_against_edge(&input, input_count, edge, clip, &mut output);
355        if output_count < 3 {
356            return;
357        }
358        input_count = output_count;
359        std::mem::swap(&mut input, &mut output);
360    }
361
362    if signed_area_vertices(&input[..input_count]).abs() <= GEOM_EPS {
363        return;
364    }
365
366    for idx in 1..input_count - 1 {
367        let a = input[0];
368        let b = input[idx];
369        let c = input[idx + 1];
370        let tri_area = cross2(b.position() - a.position(), c.position() - a.position());
371        if tri_area.abs() > GEOM_EPS {
372            emit(a, b, c);
373        }
374    }
375}
376
377/// Widget-local 2D geometry builder.
378///
379/// Coordinates passed to this builder are local to the widget rectangle that created it:
380/// `(0, 0)` is the widget's top-left corner, while `(width, height)` is the bottom-right corner.
381/// Nested clips are also widget-local and are pushed onto the shared draw-context clip stack after
382/// being translated into screen space, so a widget-local clip can only reduce visibility and can
383/// never expand beyond the area the widget already owns.
384///
385/// The builder tessellates higher-level shapes into triangles immediately and software-clips every
386/// triangle against the current widget-local clip rectangle before storing it. Because the emitted
387/// triangles are already clipped, nested local clip scopes do not need to flush the batch or emit
388/// retained clip commands.
389pub struct Graphics<'a, 'b> {
390    draw: &'a mut DrawCtx<'b>,
391    widget_rect: Recti,
392    widget_origin: Vec2f,
393    white_uv: Vec2f,
394    clip_base_depth: usize,
395    triangle_batch_start: usize,
396    triangle_batch_count: usize,
397}
398
399impl<'a, 'b> Graphics<'a, 'b> {
400    pub(crate) fn new(draw: &'a mut DrawCtx<'b>, widget_rect: Recti) -> Self {
401        Self::new_with_clip_root(draw, widget_rect, widget_rect)
402    }
403
404    // Public widget-local graphics keep their clip root inside the widget bounds. Internal widget
405    // paint adapters can supply a wider clip root to preserve legacy frame overflow behavior while
406    // still reusing the same local-coordinate drawing code.
407    pub(crate) fn new_with_clip_root(draw: &'a mut DrawCtx<'b>, widget_rect: Recti, clip_root: Recti) -> Self {
408        // The builder records how deep the shared clip stack was before it started, then pushes one
409        // root clip in screen space. All later widget-local clip changes are translated onto that
410        // same stack, and drop restores the previous depth so the outer traversal state is intact.
411        let clip_base_depth = draw.clip_depth();
412        draw.push_clip_rect(clip_root);
413        let white_rect = draw.atlas().get_icon_rect(WHITE_ICON);
414        let atlas_dim = draw.atlas().get_texture_dimension();
415        let white_uv = Vec2f::new(
416            (white_rect.x as f32 + white_rect.width as f32 * 0.5) / atlas_dim.width as f32,
417            (white_rect.y as f32 + white_rect.height as f32 * 0.5) / atlas_dim.height as f32,
418        );
419        let triangle_batch_start = draw.triangle_vertex_count();
420
421        Self {
422            draw,
423            widget_rect,
424            widget_origin: Vec2f::new(widget_rect.x as f32, widget_rect.y as f32),
425            white_uv,
426            clip_base_depth,
427            triangle_batch_start,
428            triangle_batch_count: 0,
429        }
430    }
431
432    /// Returns the widget-local rectangle available to this graphics builder.
433    ///
434    /// This is the widget's full layout rect expressed in local coordinates, regardless of parent
435    /// clipping. Use [`Graphics::current_clip_rect`] when the visible area matters.
436    pub fn local_rect(&self) -> Recti {
437        Recti::new(0, 0, self.widget_rect.width, self.widget_rect.height)
438    }
439
440    /// Returns the current widget-local clip rectangle.
441    ///
442    /// The returned rect is derived from the shared draw-context clip stack. It is therefore
443    /// already intersected with the widget root and all earlier local clip scopes.
444    pub fn current_clip_rect(&self) -> Recti {
445        self.screen_to_local_rect(self.draw.current_clip_rect())
446    }
447
448    /// Narrows the current clip by intersecting it with `rect`.
449    ///
450    /// The clip is expressed in widget-local coordinates, translated into screen space, and pushed
451    /// onto the shared draw-context stack. Because `DrawCtx::push_clip_rect` intersects against
452    /// the current top, this can never expand the visible area.
453    pub fn push_clip_rect(&mut self, rect: Recti) {
454        self.draw.push_clip_rect(self.local_to_screen_rect(rect));
455    }
456
457    /// Replaces the current clip with an intersection against `rect`.
458    ///
459    /// Unlike `push_clip_rect`, this keeps the current stack depth. The replacement is still
460    /// monotonic: it intersects with the existing top clip instead of replacing it wholesale.
461    pub fn set_clip_rect(&mut self, rect: Recti) {
462        let clip = intersect_clip_rect(self.draw.current_clip_rect(), self.local_to_screen_rect(rect));
463        self.draw.replace_current_clip_rect(clip);
464    }
465
466    /// Restores the previous widget-local clip rectangle.
467    pub fn pop_clip_rect(&mut self) {
468        if self.draw.clip_depth() > self.clip_base_depth + 1 {
469            self.draw.pop_clip_rect();
470        }
471    }
472
473    /// Executes `f` with an additional widget-local clip applied.
474    pub fn with_clip<F: FnOnce(&mut Self)>(&mut self, rect: Recti, f: F) {
475        self.push_clip_rect(rect);
476        f(self);
477        self.pop_clip_rect();
478    }
479
480    /// Fills a solid axis-aligned rectangle in widget-local coordinates.
481    ///
482    /// Rectangles are routed through the same triangle path as every other filled primitive so the
483    /// widget paint stack only has one geometry implementation to maintain.
484    pub fn draw_rect(&mut self, rect: Recti, color: Color) {
485        if rect.width <= 0 || rect.height <= 0 || color.a == 0 {
486            return;
487        }
488
489        let x0 = rect.x as f32;
490        let y0 = rect.y as f32;
491        let x1 = (rect.x + rect.width) as f32;
492        let y1 = (rect.y + rect.height) as f32;
493        self.push_quad_local(Vec2f::new(x0, y0), Vec2f::new(x1, y0), Vec2f::new(x1, y1), Vec2f::new(x0, y1), color);
494    }
495
496    /// Draws a 1-pixel outline around `rect`.
497    ///
498    /// The outline is decomposed into four filled edge rectangles so it stays on the same clipped
499    /// triangle path as every other solid primitive.
500    pub fn draw_box(&mut self, rect: Recti, color: Color) {
501        self.draw_rect(Recti::new(rect.x + 1, rect.y, rect.width - 2, 1), color);
502        self.draw_rect(Recti::new(rect.x + 1, rect.y + rect.height - 1, rect.width - 2, 1), color);
503        self.draw_rect(Recti::new(rect.x, rect.y, 1, rect.height), color);
504        self.draw_rect(Recti::new(rect.x + rect.width - 1, rect.y, 1, rect.height), color);
505    }
506
507    /// Draws text using widget-local coordinates for the glyph origin.
508    ///
509    /// Text itself still reuses the existing retained text command, but the graphics builder owns
510    /// the local-to-screen translation and the clip-state wrapping so widgets no longer have to
511    /// decide which paint API to use.
512    pub fn draw_text(&mut self, font: FontId, text: &str, pos: Vec2i, color: Color) {
513        if text.is_empty() || color.a == 0 {
514            return;
515        }
516
517        let size = self.draw.atlas().get_text_size(font, text);
518        let bounds = Recti::new(pos.x, pos.y, size.width, size.height);
519        let screen_pos = self.local_to_screen_pos(pos);
520        let text = text.to_string();
521        self.emit_clipped_command(bounds, |draw| {
522            draw.push_command(Command::Text { text, pos: screen_pos, color, font });
523        });
524    }
525
526    /// Draws one icon rectangle using widget-local coordinates.
527    pub fn draw_icon(&mut self, id: IconId, rect: Recti, color: Color) {
528        let screen_rect = self.local_to_screen_rect(rect);
529        self.emit_clipped_command(rect, |draw| {
530            draw.push_command(Command::Icon { id, rect: screen_rect, color });
531        });
532    }
533
534    /// Draws one image rectangle using widget-local coordinates.
535    pub fn draw_image(&mut self, image: Image, rect: Recti, color: Color) {
536        let screen_rect = self.local_to_screen_rect(rect);
537        self.emit_clipped_command(rect, |draw| {
538            draw.push_command(Command::Image { image, rect: screen_rect, color });
539        });
540    }
541
542    /// Re-renders a slot and then draws it using widget-local coordinates.
543    pub fn draw_slot_with_function(&mut self, id: SlotId, rect: Recti, color: Color, payload: Rc<dyn Fn(usize, usize) -> Color4b>) {
544        let screen_rect = self.local_to_screen_rect(rect);
545        self.emit_clipped_command(rect, |draw| {
546            draw.push_command(Command::SlotRedraw { id, rect: screen_rect, color, payload });
547        });
548    }
549
550    /// Draws one framed control using the current style colors.
551    pub fn draw_frame(&mut self, rect: Recti, colorid: ControlColor) {
552        let color = self.draw.style().colors[colorid as usize];
553        self.draw_rect(rect, color);
554        if colorid == ControlColor::ScrollBase || colorid == ControlColor::ScrollThumb || colorid == ControlColor::TitleBG {
555            return;
556        }
557
558        let border = self.draw.style().colors[ControlColor::Border as usize];
559        if border.a != 0 {
560            self.draw_box(expand_rect(rect, 1), border);
561        }
562    }
563
564    /// Draws one widget frame using the same focus/hover color promotion as the legacy widget
565    /// helpers.
566    pub fn draw_widget_frame(&mut self, focused: bool, hovered: bool, rect: Recti, mut colorid: ControlColor, opt: WidgetOption) {
567        if opt.has_no_frame() {
568            return;
569        }
570        if focused {
571            colorid.focus();
572        } else if hovered {
573            colorid.hover();
574        }
575        self.draw_frame(rect, colorid);
576    }
577
578    /// Draws centered or aligned control text inside `rect`.
579    ///
580    /// This reuses the shared control-text positioning helper from `DrawCtx` so widget and
581    /// container labels stay visually identical even though widgets now paint through `Graphics`.
582    pub fn draw_control_text(&mut self, text: &str, rect: Recti, colorid: ControlColor, opt: WidgetOption) {
583        self.draw_control_text_with_font(self.draw.style().font, text, rect, colorid, opt);
584    }
585
586    /// Draws centered or aligned control text using an explicit font.
587    pub fn draw_control_text_with_font(&mut self, font: FontId, text: &str, rect: Recti, colorid: ControlColor, opt: WidgetOption) {
588        let (font, color, pos) = {
589            let style = self.draw.style();
590            let atlas = self.draw.atlas();
591            (
592                font,
593                style.colors[colorid as usize],
594                control_text_position_with_font(style, atlas, font, text, rect, opt),
595            )
596        };
597        self.push_clip_rect(rect);
598        self.draw_text(font, text, pos, color);
599        self.pop_clip_rect();
600    }
601
602    /// Strokes one solid line segment with the provided width.
603    ///
604    /// The stroke is tessellated into two triangles instead of relying on platform line
605    /// primitives. That keeps behavior predictable across backends and makes rectangular clipping
606    /// behave the same way as filled polygon rendering.
607    pub fn stroke_line(&mut self, a: Vec2f, b: Vec2f, width: f32, color: Color) {
608        if width <= 0.0 || color.a == 0 {
609            return;
610        }
611
612        let delta = b - a;
613        let len_sq = delta.x * delta.x + delta.y * delta.y;
614        if len_sq <= GEOM_EPS_SQ {
615            let half = width * 0.5;
616            self.push_quad_local(
617                Vec2f::new(a.x - half, a.y - half),
618                Vec2f::new(a.x + half, a.y - half),
619                Vec2f::new(a.x + half, a.y + half),
620                Vec2f::new(a.x - half, a.y + half),
621                color,
622            );
623            return;
624        }
625
626        let inv_len = len_sq.sqrt().recip();
627        let normal = Vec2f::new(-delta.y * inv_len, delta.x * inv_len) * (width * 0.5);
628
629        let p0 = a + normal;
630        let p1 = b + normal;
631        let p2 = b - normal;
632        let p3 = a - normal;
633        self.push_quad_local(p0, p1, p2, p3, color);
634    }
635
636    /// Fills a simple polygon described in widget-local coordinates.
637    ///
638    /// Convex polygons take the fast triangle-fan path. Concave simple polygons fall back to a
639    /// compact ear-clipping triangulator implemented here to avoid pulling a heavier dependency
640    /// into the core crate. Self-intersecting polygons are intentionally unsupported.
641    pub fn fill_polygon(&mut self, points: &[Vec2f], color: Color) {
642        if points.len() < 3 || color.a == 0 {
643            return;
644        }
645
646        let mut points = dedupe_and_simplify_polygon(points);
647        if points.len() < 3 {
648            return;
649        }
650
651        let area = signed_area(points.as_slice());
652        if area.abs() <= GEOM_EPS {
653            return;
654        }
655        if area < 0.0 {
656            points.reverse();
657        }
658
659        let rgba = color4b(color.r, color.g, color.b, color.a);
660
661        if is_convex_polygon_ccw(points.as_slice()) {
662            self.push_triangle_fan(points.as_slice(), rgba);
663            return;
664        }
665
666        let mut indices: Vec<usize> = (0..points.len()).collect();
667        while indices.len() > 3 {
668            let mut ear_found = false;
669
670            for idx in 0..indices.len() {
671                let prev = indices[(idx + indices.len() - 1) % indices.len()];
672                let curr = indices[idx];
673                let next = indices[(idx + 1) % indices.len()];
674                let a = points[prev];
675                let b = points[curr];
676                let c = points[next];
677
678                if !is_convex_ccw(a, b, c) {
679                    continue;
680                }
681
682                let mut contains_other = false;
683                for probe in &indices {
684                    if *probe == prev || *probe == curr || *probe == next {
685                        continue;
686                    }
687                    if point_in_triangle_ccw(points[*probe], a, b, c) {
688                        contains_other = true;
689                        break;
690                    }
691                }
692                if contains_other {
693                    continue;
694                }
695
696                self.push_triangle_local(a, b, c, rgba);
697                indices.remove(idx);
698                ear_found = true;
699                break;
700            }
701
702            if !ear_found {
703                return;
704            }
705        }
706
707        if indices.len() == 3 {
708            self.push_triangle_local(points[indices[0]], points[indices[1]], points[indices[2]], rgba);
709        }
710    }
711
712    // Converts the current widget-local clip into the screen-space clip consumed by retained text,
713    // icon, image, and slot commands.
714    fn current_screen_clip_rect(&self) -> Recti {
715        self.draw.current_clip_rect()
716    }
717
718    // Converts widget-local integer positions into the screen-space coordinates used by the rest
719    // of the retained command stream.
720    fn local_to_screen_pos(&self, pos: Vec2i) -> Vec2i {
721        pos + Vec2i::new(self.widget_rect.x, self.widget_rect.y)
722    }
723
724    // Converts widget-local integer rectangles into screen-space rectangles while preserving
725    // extents.
726    fn local_to_screen_rect(&self, rect: Recti) -> Recti {
727        translate_rect(rect, Vec2i::new(self.widget_rect.x, self.widget_rect.y))
728    }
729
730    // Converts the shared screen-space clip back into widget-local coordinates so the tessellator
731    // can software-clip generated triangles before they ever reach the retained command stream.
732    fn screen_to_local_rect(&self, rect: Recti) -> Recti {
733        translate_rect(rect, Vec2i::new(-self.widget_rect.x, -self.widget_rect.y))
734    }
735
736    // Flushes any pending triangle batch, then emits a retained non-triangle command clipped
737    // against the current local clip rect. This keeps ordering correct when widgets mix text,
738    // images, and solid geometry inside one graphics builder.
739    fn emit_clipped_command<F>(&mut self, bounds_local: Recti, emit: F)
740    where
741        F: FnOnce(&mut DrawCtx<'b>),
742    {
743        self.flush_batch();
744        let clip = self.current_screen_clip_rect();
745        let bounds = self.local_to_screen_rect(bounds_local);
746        self.draw.emit_clipped(bounds, clip, emit);
747    }
748
749    // Emits a convex polygon as a triangle fan rooted at the first point. This is the fastest path
750    // for fills and avoids any temporary index bookkeeping.
751    fn push_triangle_fan(&mut self, points: &[Vec2f], color: Color4b) {
752        for idx in 1..points.len() - 1 {
753            self.push_triangle_local(points[0], points[idx], points[idx + 1], color);
754        }
755    }
756
757    // Reuses the triangle path for thick-line quads and other four-corner shapes. Keeping quads as
758    // two triangles avoids a separate code path in the retained command stream and the backends.
759    fn push_quad_local(&mut self, p0: Vec2f, p1: Vec2f, p2: Vec2f, p3: Vec2f, color: Color) {
760        let rgba = color4b(color.r, color.g, color.b, color.a);
761        self.push_triangle_local(p0, p1, p2, rgba);
762        self.push_triangle_local(p0, p2, p3, rgba);
763    }
764
765    // Clips one local triangle against the current local clip and appends the surviving triangles
766    // into the shared container-owned arena. Clipping here means later clip-stack changes no
767    // longer need to fragment the retained command stream.
768    fn push_triangle_local(&mut self, a: Vec2f, b: Vec2f, c: Vec2f, color: Color4b) {
769        let clip = self.current_clip_rect();
770        let widget_origin = self.widget_origin;
771        clip_triangle_vertices_to_rect(
772            Vertex::new(a, self.white_uv, color),
773            Vertex::new(b, self.white_uv, color),
774            Vertex::new(c, self.white_uv, color),
775            clip,
776            |va, vb, vc| {
777                self.draw.push_triangle_vertices(
778                    translate_vertex(va, widget_origin),
779                    translate_vertex(vb, widget_origin),
780                    translate_vertex(vc, widget_origin),
781                );
782                self.triangle_batch_count += 3;
783            },
784        );
785    }
786
787    // Finalizes the current triangle batch. At this point every triangle has already been clipped
788    // in software, so replay only needs the range into the shared arena and no extra clip-state
789    // changes.
790    fn flush_batch(&mut self) {
791        if self.triangle_batch_count == 0 {
792            return;
793        }
794
795        self.draw.push_command(Command::Triangle {
796            vertex_start: self.triangle_batch_start,
797            vertex_count: self.triangle_batch_count,
798        });
799        self.triangle_batch_start = self.draw.triangle_vertex_count();
800        self.triangle_batch_count = 0;
801    }
802}
803
804impl<'a, 'b> Drop for Graphics<'a, 'b> {
805    fn drop(&mut self) {
806        self.flush_batch();
807        self.draw.pop_clip_rect_to(self.clip_base_depth);
808    }
809}
810
811#[cfg(test)]
812mod tests {
813    use super::*;
814    use crate::container::Command;
815    use crate::draw_context::clip_relation;
816
817    fn assert_rect_eq(actual: Recti, expected: Recti) {
818        assert_eq!(
819            (actual.x, actual.y, actual.width, actual.height),
820            (expected.x, expected.y, expected.width, expected.height)
821        );
822    }
823
824    fn assert_vec2_eq(actual: Vec2f, expected: Vec2f) {
825        assert!((actual.x - expected.x).abs() <= GEOM_EPS);
826        assert!((actual.y - expected.y).abs() <= GEOM_EPS);
827    }
828
829    fn make_vertex(pos: (f32, f32)) -> Vertex {
830        Vertex::new(Vec2f::new(pos.0, pos.1), Vec2f::default(), color4b(255, 255, 255, 255))
831    }
832
833    #[test]
834    fn clip_relation_reports_partial_overlap() {
835        let clip = rect(10, 10, 10, 10);
836        let bounds = rect(5, 5, 10, 10);
837        assert_eq!(clip_relation(bounds, clip) as u32, Clip::Part as u32);
838    }
839
840    #[test]
841    fn triangle_bounds_are_conservative() {
842        let bounds = rect_from_points(&[Vec2f::new(1.2, 2.6), Vec2f::new(4.8, 3.1), Vec2f::new(3.0, 9.9)]);
843        assert_rect_eq(bounds, rect(1, 2, 4, 8));
844    }
845
846    #[test]
847    fn polygon_cleanup_removes_duplicate_closing_point() {
848        let points = dedupe_and_simplify_polygon(&[
849            Vec2f::new(0.0, 0.0),
850            Vec2f::new(10.0, 0.0),
851            Vec2f::new(10.0, 10.0),
852            Vec2f::new(0.0, 10.0),
853            Vec2f::new(0.0, 0.0),
854        ]);
855        assert_eq!(points.len(), 4);
856    }
857
858    #[test]
859    fn local_rect_translation_is_preserved_in_emitted_vertices() {
860        let atlas = AtlasHandle::from(&AtlasSource {
861            width: 1,
862            height: 1,
863            pixels: &[255, 255, 255, 255],
864            icons: &[("white", Recti::new(0, 0, 1, 1))],
865            fonts: &[],
866            format: SourceFormat::Raw,
867            slots: &[],
868        });
869        let style = Style::default();
870        let mut commands = Vec::new();
871        let mut triangle_vertices = Vec::new();
872        let mut clip_stack = vec![rect(0, 0, 200, 200)];
873        let mut draw = DrawCtx::new(&mut commands, &mut triangle_vertices, &mut clip_stack, &style, &atlas);
874        {
875            let mut graphics = Graphics::new(&mut draw, rect(20, 30, 50, 50));
876            graphics.push_triangle_local(Vec2f::new(0.0, 0.0), Vec2f::new(10.0, 0.0), Vec2f::new(0.0, 10.0), color4b(255, 255, 255, 255));
877        }
878
879        match &commands[0] {
880            Command::Triangle { vertex_start, vertex_count } => {
881                let vertices = &triangle_vertices[*vertex_start..*vertex_start + *vertex_count];
882                let a = vertices[0].position();
883                let b = vertices[1].position();
884                let c = vertices[2].position();
885                assert_vec2_eq(a, Vec2f::new(20.0, 30.0));
886                assert_vec2_eq(b, Vec2f::new(30.0, 30.0));
887                assert_vec2_eq(c, Vec2f::new(20.0, 40.0));
888            }
889            _ => panic!("expected triangle command"),
890        }
891    }
892
893    #[test]
894    fn local_clip_changes_stay_in_one_triangle_batch() {
895        let atlas = AtlasHandle::from(&AtlasSource {
896            width: 1,
897            height: 1,
898            pixels: &[255, 255, 255, 255],
899            icons: &[("white", Recti::new(0, 0, 1, 1))],
900            fonts: &[],
901            format: SourceFormat::Raw,
902            slots: &[],
903        });
904        let style = Style::default();
905        let mut commands = Vec::new();
906        let mut triangle_vertices = Vec::new();
907        let mut clip_stack = vec![rect(0, 0, 200, 200)];
908        let mut draw = DrawCtx::new(&mut commands, &mut triangle_vertices, &mut clip_stack, &style, &atlas);
909        {
910            let mut graphics = Graphics::new(&mut draw, rect(0, 0, 50, 50));
911            graphics.stroke_line(Vec2f::new(0.0, 0.0), Vec2f::new(10.0, 0.0), 2.0, color(255, 0, 0, 255));
912            graphics.push_clip_rect(rect(0, 0, 5, 5));
913            graphics.stroke_line(Vec2f::new(0.0, 2.0), Vec2f::new(10.0, 2.0), 2.0, color(255, 0, 0, 255));
914        }
915
916        let triangle_count = commands.iter().filter(|cmd| matches!(cmd, Command::Triangle { .. })).count();
917        let clip_count = commands.iter().filter(|cmd| matches!(cmd, Command::Clip { .. })).count();
918        assert_eq!(triangle_count, 1);
919        assert_eq!(clip_count, 0);
920    }
921
922    #[test]
923    fn graphics_restores_shared_clip_stack_on_drop() {
924        let atlas = AtlasHandle::from(&AtlasSource {
925            width: 1,
926            height: 1,
927            pixels: &[255, 255, 255, 255],
928            icons: &[("white", Recti::new(0, 0, 1, 1))],
929            fonts: &[],
930            format: SourceFormat::Raw,
931            slots: &[],
932        });
933        let style = Style::default();
934        let mut commands = Vec::new();
935        let mut triangle_vertices = Vec::new();
936        let mut clip_stack = vec![rect(0, 0, 200, 200)];
937        let mut draw = DrawCtx::new(&mut commands, &mut triangle_vertices, &mut clip_stack, &style, &atlas);
938        {
939            let mut graphics = Graphics::new(&mut draw, rect(20, 30, 50, 50));
940            graphics.push_clip_rect(rect(0, 0, 5, 5));
941            assert_rect_eq(graphics.current_clip_rect(), rect(0, 0, 5, 5));
942        }
943
944        assert_rect_eq(draw.current_clip_rect(), rect(0, 0, 200, 200));
945    }
946
947    #[test]
948    fn local_triangles_are_software_clipped_before_emission() {
949        let atlas = AtlasHandle::from(&AtlasSource {
950            width: 1,
951            height: 1,
952            pixels: &[255, 255, 255, 255],
953            icons: &[("white", Recti::new(0, 0, 1, 1))],
954            fonts: &[],
955            format: SourceFormat::Raw,
956            slots: &[],
957        });
958        let style = Style::default();
959        let mut commands = Vec::new();
960        let mut triangle_vertices = Vec::new();
961        let mut clip_stack = vec![rect(0, 0, 200, 200)];
962        let mut draw = DrawCtx::new(&mut commands, &mut triangle_vertices, &mut clip_stack, &style, &atlas);
963        {
964            let mut graphics = Graphics::new(&mut draw, rect(20, 30, 50, 50));
965            graphics.push_clip_rect(rect(0, 0, 5, 5));
966            graphics.stroke_line(Vec2f::new(-10.0, 2.0), Vec2f::new(20.0, 2.0), 2.0, color(255, 0, 0, 255));
967        }
968
969        match &commands[0] {
970            Command::Triangle { vertex_start, vertex_count } => {
971                let vertices = &triangle_vertices[*vertex_start..*vertex_start + *vertex_count];
972                assert!(!vertices.is_empty());
973                for vertex in vertices {
974                    let pos = vertex.position();
975                    assert!(pos.x >= 20.0 - GEOM_EPS && pos.x <= 25.0 + GEOM_EPS);
976                    assert!(pos.y >= 30.0 - GEOM_EPS && pos.y <= 35.0 + GEOM_EPS);
977                }
978            }
979            _ => panic!("expected triangle command"),
980        }
981    }
982
983    #[test]
984    fn point_in_triangle_accepts_boundary_points() {
985        let a = Vec2f::new(0.0, 0.0);
986        let b = Vec2f::new(10.0, 0.0);
987        let c = Vec2f::new(0.0, 10.0);
988        assert!(point_in_triangle_ccw(Vec2f::new(5.0, 0.0), a, b, c));
989    }
990
991    #[test]
992    fn helper_vertices_are_constructible() {
993        let vertex = make_vertex((1.0, 2.0));
994        assert_vec2_eq(vertex.position(), Vec2f::new(1.0, 2.0));
995    }
996}