Skip to main content

kui_core/
deco.rs

1//! Decoration lines that are not a rect: the wavy and dotted
2//! underlines a text, a span or a cell asks for. A solid line is one
3//! `Solid` quad, as it always was; these are runs of
4//! [`QuadKind::Segment`] — the capsule the backend already draws for a
5//! `line` node — so no backend, header or protocol learns a
6//! kind. A wave is a zigzag of short pieces whose round caps soften the
7//! corners; a dotted line is a row of zero-length pieces, which the
8//! capsule SDF draws as dots. Both are sized from the stroke the face
9//! recommends, so they scale with the text and the display.
10//!
11//! The cost is quads: a wave is two pieces per period of six strokes, a
12//! dotted line one per four. A screenful of diagnostics under 14 px text
13//! at scale 2 — say thirty runs of twenty characters — is about 1,700
14//! quads, a fifth of a 10k-rect frame's; a `QuadKind` of its own is the
15//! next step if a profile ever shows that.
16
17use crate::color::Color;
18use crate::display::{ClipId, Quad, QuadKind};
19use crate::geom::{Rect, Vec2};
20use crate::spec::UnderlineStyle;
21
22/// Pushes the quads for a line of `style` where a solid underline's rect
23/// would be `(x, y, w, stroke)` in physical px: a solid line is that rect,
24/// the others are built around its centre.
25#[allow(clippy::too_many_arguments)]
26pub(crate) fn push_line(
27    out: &mut Vec<Quad>,
28    style: UnderlineStyle,
29    x: f32,
30    y: f32,
31    w: f32,
32    stroke: f32,
33    color: Color,
34    clip: ClipId,
35) {
36    if w <= 0.0 {
37        return;
38    }
39    let stroke = stroke.max(1.0);
40    match style {
41        UnderlineStyle::Solid => out.push(Quad {
42            rect: Rect::new(x, y, w, stroke),
43            color,
44            border_color: Color::TRANSPARENT,
45            radius: crate::display::SQUARE,
46            border_w: 0.0,
47            blur: 0.0,
48            kind: QuadKind::Solid,
49            clip,
50            uv: [0; 4],
51        }),
52        UnderlineStyle::Wavy => {
53            // Peaks a stroke above and below the line's centre, half a
54            // period apart: the wave's height is three strokes, its
55            // period six. The last piece ends at the run's edge, on the
56            // slope it was on.
57            let cy = y + stroke * 0.5;
58            let amp = stroke;
59            let half = stroke * 3.0;
60            let mut px = x;
61            let mut py = cy + amp;
62            let mut up = true;
63            let end = x + w;
64            while px < end {
65                let nx = (px + half).min(end);
66                let ny = if up { cy - amp } else { cy + amp };
67                let ny = if nx < px + half {
68                    py + (ny - py) * ((nx - px) / half)
69                } else {
70                    ny
71                };
72                push_piece(
73                    out,
74                    Vec2::new(px, py),
75                    Vec2::new(nx, ny),
76                    stroke,
77                    color,
78                    clip,
79                );
80                px = nx;
81                py = ny;
82                up = !up;
83            }
84        }
85        UnderlineStyle::Dotted => {
86            // Dots two strokes across, four apart, centred on the line.
87            let cy = y + stroke * 0.5;
88            let step = stroke * 4.0;
89            let mut px = x + stroke;
90            while px + stroke <= x + w + 0.5 {
91                let p = Vec2::new(px, cy);
92                push_piece(out, p, p, stroke * 2.0, color, clip);
93                px += step;
94            }
95        }
96    }
97}
98
99/// One capsule piece from `a` to `b`, `width` across, physical px — the
100/// shape `runtime::emit::push_segments` makes for a `line` node, boxed the
101/// same way so the backend's edge ramp is never cut by the quad's edge.
102fn push_piece(out: &mut Vec<Quad>, a: Vec2, b: Vec2, width: f32, color: Color, clip: ClipId) {
103    let pad = width * 0.5 + 2.0;
104    let (x0, x1) = (a.x.min(b.x) - pad, a.x.max(b.x) + pad);
105    let (y0, y1) = (a.y.min(b.y) - pad, a.y.max(b.y) + pad);
106    out.push(Quad {
107        rect: Rect::new(x0, y0, x1 - x0, y1 - y0),
108        color,
109        border_color: Color::TRANSPARENT,
110        radius: crate::display::SQUARE,
111        border_w: width,
112        blur: 0.0,
113        kind: QuadKind::Segment,
114        clip,
115        uv: Quad::segment_uv([a.x, a.y, b.x, b.y]),
116    });
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    fn quads(style: UnderlineStyle, w: f32) -> Vec<Quad> {
124        let mut out = Vec::new();
125        push_line(&mut out, style, 10.0, 100.0, w, 1.0, Color::WHITE, 0);
126        out
127    }
128
129    #[test]
130    fn a_solid_line_is_the_rect_it_always_was() {
131        let q = quads(UnderlineStyle::Solid, 50.0);
132        assert_eq!(q.len(), 1);
133        assert_eq!(q[0].kind, QuadKind::Solid);
134        assert_eq!(q[0].rect, Rect::new(10.0, 100.0, 50.0, 1.0));
135    }
136
137    #[test]
138    fn a_wave_is_pieces_that_meet_and_end_at_the_edge() {
139        let q = quads(UnderlineStyle::Wavy, 20.0);
140        // Six-stroke period, half a period a piece: 20 px is six pieces
141        // and a partial seventh.
142        assert_eq!(q.len(), 7);
143        assert!(q.iter().all(|q| q.kind == QuadKind::Segment));
144        let ends: Vec<[f32; 4]> = q.iter().map(Quad::segment_ends).collect();
145        for pair in ends.windows(2) {
146            assert_eq!(
147                (pair[0][2], pair[0][3]),
148                (pair[1][0], pair[1][1]),
149                "pieces meet"
150            );
151        }
152        assert_eq!(ends[0][0], 10.0);
153        assert_eq!(ends[6][2], 30.0, "the last piece ends at the run's edge");
154        // Peaks a stroke either side of the centre, y = 100.5: the first
155        // piece rises from the lower peak to the upper one.
156        assert_eq!(ends[0][1], 101.5);
157        assert_eq!(ends[0][3], 99.5);
158        assert_eq!(ends[1][3], 101.5);
159    }
160
161    #[test]
162    fn dots_are_zero_length_pieces_four_strokes_apart() {
163        let q = quads(UnderlineStyle::Dotted, 20.0);
164        assert_eq!(q.len(), 5);
165        for (i, q) in q.iter().enumerate() {
166            let [x0, y0, x1, y1] = q.segment_ends();
167            assert_eq!((x0, y0), (x1, y1), "a dot");
168            assert_eq!(x0, 11.0 + 4.0 * i as f32);
169            assert_eq!(q.border_w, 2.0);
170        }
171    }
172
173    #[test]
174    fn nothing_for_an_empty_run() {
175        assert!(quads(UnderlineStyle::Wavy, 0.0).is_empty());
176        assert!(quads(UnderlineStyle::Dotted, 0.0).is_empty());
177    }
178}