rust_widgets 2.8.4

Pure Rust cross-platform native GUI library with hardware-adaptive rendering, 180 widgets, touch/gesture support, i18n, and SVG-pipeline-accurate output
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
// SPDX-FileCopyrightText: Copyright (c) 2026 Mike Li/Mikewolfli/Wei Li(mikewolfli@163.com)
// SPDX-License-Identifier: MIT

use crate::compat::Vec;
use crate::core::{Color, Point};
/// Which geometry a [`Gradient`] interpolates along.
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum GradientType {
    /// Colour is interpolated along the straight line from `start_point` to
    /// `end_point`. The default.
    #[default]
    Linear,
    /// Colour is interpolated by distance from `center`, reaching the last stop
    /// at `radius`.
    Radial,
    /// Colour is interpolated by angle (swept) around `center`.
    Conic,
}
/// One colour stop on a gradient ramp.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct GradientStop {
    /// Where the stop sits along the ramp, as a fraction in the inclusive range
    /// `0.0`..=`1.0`: `0.0` is the ramp's start, `1.0` its end. Values outside
    /// that range cannot be constructed through [`GradientStop::new`], but the
    /// field is public and a directly-constructed stop may hold one.
    ///
    /// # Non-finite values (D09-STYLE-03)
    ///
    /// The field is public, so a stop built with struct-literal syntax can still hold `NaN` or
    /// `±∞`. [`Gradient::with_stops`] normalises every stop's position through
    /// [`GradientStop::normalized_position`] before sorting, so the invariant
    /// [`Gradient::interpolate`] relies on holds for collect/*builder* input too. A vector
    /// assigned *directly* through [`Gradient::stops`] is the caller's responsibility, exactly
    /// as its ordering is.
    pub position: f32,
    /// The colour reached exactly at `position`.
    pub color: Color,
}
impl GradientStop {
    /// Creates a stop, clamping `position` into `0.0`..=`1.0`.
    ///
    /// # Non-finite positions (D09-STYLE-03)
    ///
    /// [`f32::clamp`] passes `NaN` through unchanged, so a plain `clamp` violated this
    /// constructor's documented range contract and left a stop whose position compares as
    /// neither inside nor outside the ramp. The policy is therefore: **`NaN` normalises to
    /// `0.0`** (the ramp's start, the deterministic value an unset fraction reads as) and
    /// `±∞` clamps to the nearer end. Apply it through [`GradientStop::normalized_position`] so
    /// every entry point — this constructor, [`Gradient::interpolate`] and
    /// [`Gradient::reverse`] — agrees.
    pub fn new(position: f32, color: Color) -> Self {
        Self { position: Self::normalized_position(position), color }
    }

    /// The documented non-finite policy for a ramp position, shared by every entry point
    /// (D09-STYLE-03).
    ///
    /// `NaN` → `0.0`; `±∞` → `0.0` / `1.0`; a finite value → clamped into `0.0..=1.0`.
    pub fn normalized_position(position: f32) -> f32 {
        if position.is_nan() {
            0.0
        } else {
            position.clamp(0.0, 1.0)
        }
    }
}
/// A colour ramp, plus the geometry that says how ramp positions map to space.
///
/// Exactly one of the three geometries is in effect at a time, selected by
/// `gradient_type`; the fields belonging to the other two are left at their
/// constructor defaults and are ignored.
///
/// [`Gradient::interpolate`] samples the ramp by a normalised position and does
/// not itself depend on `gradient_type` — mapping a ramp position to a point in
/// space is the renderer's job, using the geometry fields below.
#[derive(Debug, Clone, PartialEq)]
pub struct Gradient {
    /// Which geometry the ramp is laid out along.
    pub gradient_type: GradientType,
    /// The ramp's colour stops. Always kept sorted by ascending `position` by
    /// the builder methods on this type; [`Gradient::interpolate`] relies on that
    /// ordering, so a vector assigned directly through this field must be sorted
    /// by the caller.
    pub stops: Vec<GradientStop>,
    /// Start of the ramp for [`GradientType::Linear`]: the point at position
    /// `0.0`. Unused by the radial and conic geometries.
    pub start_point: Point,
    /// End of the ramp for [`GradientType::Linear`]: the point at position
    /// `1.0`. Unused by the radial and conic geometries.
    pub end_point: Point,
    /// Sweep angle in **degrees**, clockwise from the positive x-axis. Settable
    /// for the linear geometry via [`Gradient::with_angle`]; for
    /// [`GradientType::Conic`] it is the angle the sweep starts at and is set by
    /// [`Gradient::conic`]. Unused by the radial geometry.
    pub angle: f32,
    /// Centre point, in the parent surface's pixel coordinates, of a radial or
    /// conic gradient. Unused by the linear geometry.
    pub center: Point,
    /// Radius in **pixels** at which a radial gradient reaches its last stop.
    /// Unused by the linear and conic geometries.
    pub radius: f32,
}
impl Gradient {
    /// Creates a linear gradient running from `start` (ramp position `0.0`) to
    /// `end` (position `1.0`).
    ///
    /// The ramp starts empty: add stops with [`Gradient::add_stop`] or
    /// [`Gradient::with_stops`]. An empty ramp interpolates to
    /// [`Color::TRANSPARENT`].
    pub fn linear(start: Point, end: Point) -> Self {
        Self {
            gradient_type: GradientType::Linear,
            stops: Vec::new(),
            start_point: start,
            end_point: end,
            angle: 0.0,
            center: Point::new(0, 0),
            radius: 0.0,
        }
    }
    /// Creates a radial gradient centred on `center` whose last stop is reached
    /// at `radius` pixels from that centre.
    ///
    /// The ramp starts empty; see [`Gradient::linear`] for how to fill it.
    pub fn radial(center: Point, radius: f32) -> Self {
        Self {
            gradient_type: GradientType::Radial,
            stops: Vec::new(),
            start_point: Point::new(0, 0),
            end_point: Point::new(0, 0),
            angle: 0.0,
            center,
            radius,
        }
    }
    /// Creates a conic (swept) gradient centred on `center`, starting its sweep
    /// at `angle` **degrees** clockwise from the positive x-axis.
    ///
    /// The ramp starts empty; see [`Gradient::linear`] for how to fill it.
    pub fn conic(center: Point, angle: f32) -> Self {
        Self {
            gradient_type: GradientType::Conic,
            stops: Vec::new(),
            start_point: Point::new(0, 0),
            end_point: Point::new(0, 0),
            angle,
            center,
            radius: 0.0,
        }
    }
    /// Sets the sweep angle in **degrees** and returns `self`, for chaining onto
    /// a constructor.
    pub fn with_angle(mut self, angle: f32) -> Self {
        self.angle = angle;
        self
    }
    /// Appends a colour stop and returns `self`.
    ///
    /// `position` is the fraction along the ramp, clamped into `0.0`..=`1.0`.
    /// The stop list is re-sorted by position, so stops may be added in any
    /// order. Two stops at the same position are allowed; which of them wins at
    /// exactly that position is then left to sort stability rather than to the
    /// caller.
    pub fn add_stop(mut self, position: f32, color: Color) -> Self {
        self.stops.push(GradientStop::new(position, color));
        self.stops.sort_by(|a, b| a.position.total_cmp(&b.position));
        self
    }
    /// Replaces the whole stop list, normalises each stop's position into `0.0`..=`1.0`, sorts
    /// by ascending position, and returns `self`.
    ///
    /// Normalisation uses [`GradientStop::normalized_position`], so a stop built through a
    /// struct literal with `NaN`/`±∞` does not escape the range contract or the sort (D09-STYLE-03).
    pub fn with_stops(mut self, mut stops: Vec<GradientStop>) -> Self {
        for stop in &mut stops {
            stop.position = GradientStop::normalized_position(stop.position);
        }
        self.stops = stops;
        self.stops.sort_by(|a, b| a.position.total_cmp(&b.position));
        self
    }
    /// Samples the ramp at `position`, a fraction along it that is clamped into
    /// `0.0`..=`1.0`.
    ///
    /// The value is found by straight linear interpolation in non-premultiplied
    /// RGBA, one `u8` channel at a time, so partially transparent stops blend
    /// their alpha as a plain channel.
    ///
    /// Degenerate ramps return a defined colour rather than panicking:
    ///
    /// * no stops — [`Color::TRANSPARENT`];
    /// * exactly one stop — that stop's colour, at every position;
    /// * `position` before the first stop or at/after the last — the first or
    ///   last stop's colour, with no extension or wrapping.
    pub fn interpolate(&self, position: f32) -> Color {
        // D09-STYLE-03: `clamp` leaves `NaN` as `NaN`, which then fails every boundary
        // comparison below and fell through to the last stop's colour. Normalise the policy in
        // one place so a non-finite sample reads as the ramp's start instead of an arbitrary end.
        let position = GradientStop::normalized_position(position);
        if self.stops.is_empty() {
            return Color::TRANSPARENT;
        }
        if self.stops.len() == 1 {
            return self.stops[0].color;
        }
        if position <= self.stops[0].position {
            return self.stops[0].color;
        }
        if position >= self.stops[self.stops.len() - 1].position {
            return self.stops[self.stops.len() - 1].color;
        }
        for i in 0..self.stops.len() - 1 {
            let current = &self.stops[i];
            let next = &self.stops[i + 1];
            if position >= current.position && position <= next.position {
                let t = (position - current.position) / (next.position - current.position);
                return Self::interpolate_color(current.color, next.color, t);
            }
        }
        self.stops[self.stops.len() - 1].color
    }
    fn interpolate_color(from: Color, to: Color, t: f32) -> Color {
        // Kept hand-rolled rather than routed through `Color::blend`: `blend` goes through
        // `Color::from_f32`, which **rounds**, while this truncates via `as u8`. The two agree only
        // when the interpolated channel is an exact integer, so unifying them changes rendered
        // pixels (measured: a 50% stop moved 127 -> 128). Rendering must be byte-identical, so the
        // arithmetic stays as it is; `Color::blend` is the canonical lerp for *non-rendering* use.
        let t = t.clamp(0.0, 1.0);
        let r = ((1.0 - t) * from.r as f32 + t * to.r as f32) as u8;
        let g = ((1.0 - t) * from.g as f32 + t * to.g as f32) as u8;
        let b = ((1.0 - t) * from.b as f32 + t * to.b as f32) as u8;
        let a = ((1.0 - t) * from.a as f32 + t * to.a as f32) as u8;
        Color::rgba(r, g, b, a)
    }
    /// Returns a copy whose ramp runs the other way: the stop order is reversed
    /// and each stop's `position` is mirrored to `1.0 - position`.
    ///
    /// The geometry fields (`start_point`, `end_point`, `center`, `radius`,
    /// `angle`) are copied through unchanged, so this flips only the colour ramp,
    /// not the direction in space in which it is painted.
    pub fn reverse(&self) -> Self {
        let mut reversed = self.clone();
        reversed.stops.reverse();
        for stop in &mut reversed.stops {
            stop.position = 1.0 - stop.position;
        }
        reversed
    }
    /// Whether this gradient has enough stops to be worth painting.
    ///
    /// Reports `true` for two or more stops. A one-stop gradient paints a single
    /// flat colour — [`Gradient::interpolate`] handles it — so it is reported as
    /// invalid here; callers wanting a flat fill should use a solid colour.
    pub fn is_valid(&self) -> bool {
        self.stops.len() >= 2
    }
}
impl Default for Gradient {
    fn default() -> Self {
        Self::linear(Point::new(0, 0), Point::new(100, 0))
    }
}

/// Chainable builder for a [`Gradient`].
///
/// Differs from the [`Gradient`] `with_*`/`add_stop` methods in one way that
/// matters: [`GradientBuilder::build`] sorts the stops, so stops can be pushed
/// here in any order and the ordering invariant [`Gradient::interpolate`] relies
/// on is established once, at the end, instead of on every insertion.
pub struct GradientBuilder {
    gradient: Gradient,
}
impl GradientBuilder {
    /// Starts building a linear gradient from `start` (ramp position `0.0`) to
    /// `end` (position `1.0`).
    pub fn linear(start: Point, end: Point) -> Self {
        Self { gradient: Gradient::linear(start, end) }
    }
    /// Starts building a radial gradient centred on `center` with its last stop
    /// at `radius` **pixels** from that centre.
    pub fn radial(center: Point, radius: f32) -> Self {
        Self { gradient: Gradient::radial(center, radius) }
    }
    /// Starts building a conic (swept) gradient centred on `center`, sweeping
    /// from `angle` **degrees** clockwise from the positive x-axis.
    pub fn conic(center: Point, angle: f32) -> Self {
        Self { gradient: Gradient::conic(center, angle) }
    }
    /// Adds a colour stop at `position`, a fraction along the ramp clamped into
    /// `0.0`..=`1.0`, and returns the builder for chaining.
    ///
    /// Unlike [`Gradient::add_stop`], stops are not sorted here — the sort
    /// happens once in [`GradientBuilder::build`] — so this is the cheaper call
    /// in a loop.
    pub fn stop(mut self, position: f32, color: Color) -> Self {
        self.gradient.stops.push(GradientStop::new(position, color));
        self
    }
    /// Sorts the accumulated stops by ascending position and yields the
    /// gradient. Safe to call with no stops, which produces an empty
    /// (transparent) ramp.
    pub fn build(mut self) -> Gradient {
        self.gradient.stops.sort_by(|a, b| a.position.total_cmp(&b.position));
        self.gradient
    }
}
#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn test_gradient_creation() {
        let gradient = Gradient::linear(Point::new(0, 0), Point::new(100, 0))
            .add_stop(0.0, Color::RED)
            .add_stop(1.0, Color::BLUE);
        assert_eq!(gradient.gradient_type, GradientType::Linear);
        assert_eq!(gradient.stops.len(), 2);
    }
    #[test]
    fn test_gradient_interpolation() {
        let gradient = Gradient::linear(Point::new(0, 0), Point::new(100, 0))
            .add_stop(0.0, Color { r: 255, g: 0, b: 0, a: 255 })
            .add_stop(1.0, Color { r: 0, g: 0, b: 255, a: 255 });
        let mid_color = gradient.interpolate(0.5);
        assert_eq!(mid_color.r, 127);
        assert_eq!(mid_color.g, 0);
        assert_eq!(mid_color.b, 127);
    }
    #[test]
    fn test_gradient_reverse() {
        let gradient = Gradient::linear(Point::new(0, 0), Point::new(100, 0))
            .add_stop(0.0, Color::RED)
            .add_stop(1.0, Color::BLUE);
        let reversed = gradient.reverse();
        assert_eq!(reversed.stops[0].color, Color::BLUE);
        assert_eq!(reversed.stops[1].color, Color::RED);
    }
    #[test]
    fn gradient_linear_interpolation() {
        let g = Gradient::linear(Point::new(0, 0), Point::new(100, 0))
            .add_stop(0.0, Color::BLACK)
            .add_stop(1.0, Color::WHITE);
        assert_eq!(g.interpolate(0.0), Color::BLACK);
        assert_eq!(g.interpolate(1.0), Color::WHITE);
        let mid = g.interpolate(0.5);
        assert!(mid.r >= 125 && mid.r <= 131);
    }
    #[test]
    fn gradient_radial_creation() {
        let g = Gradient::radial(Point::new(50, 50), 30.0)
            .add_stop(0.0, Color::RED)
            .add_stop(1.0, Color::BLUE);
        assert_eq!(g.gradient_type, GradientType::Radial);
        assert_eq!(g.center, Point::new(50, 50));
        assert!((g.radius - 30.0).abs() < 1e-6);
    }
    #[test]
    fn gradient_interpolate_clamps() {
        let g = Gradient::linear(Point::new(0, 0), Point::new(100, 0))
            .add_stop(0.0, Color::BLACK)
            .add_stop(1.0, Color::WHITE);
        assert_eq!(g.interpolate(-0.5), Color::BLACK);
        assert_eq!(g.interpolate(1.5), Color::WHITE);
    }
    #[test]
    fn gradient_single_stop() {
        let g = Gradient::linear(Point::new(0, 0), Point::new(100, 0)).add_stop(0.0, Color::RED);
        assert_eq!(g.interpolate(0.0), Color::RED);
        assert_eq!(g.interpolate(0.5), Color::RED);
        assert_eq!(g.interpolate(1.0), Color::RED);
    }
    #[test]
    fn gradient_empty_stops_interpolates_transparent() {
        let g = Gradient::linear(Point::new(0, 0), Point::new(100, 0));
        assert_eq!(g.interpolate(0.0), Color::TRANSPARENT);
        assert_eq!(g.interpolate(0.5), Color::TRANSPARENT);
    }
    #[test]
    fn test_gradient_builder() {
        let gradient = GradientBuilder::linear(Point::new(0, 0), Point::new(100, 100))
            .stop(0.0, Color::WHITE)
            .stop(0.5, Color::GRAY)
            .stop(1.0, Color::BLACK)
            .build();
        assert_eq!(gradient.stops.len(), 3);
    }

    // ── D09-STYLE-03: non-finite position policy ──

    /// The constructor promises a position in `0.0..=1.0`; `NaN` and `±∞` must be normalised
    /// rather than passed through, and finite out-of-range values must still clamp.
    #[test]
    fn gradient_stop_new_normalizes_non_finite_positions() {
        assert_eq!(GradientStop::new(f32::NAN, Color::RED).position, 0.0);
        assert_eq!(GradientStop::new(f32::INFINITY, Color::RED).position, 1.0);
        assert_eq!(GradientStop::new(f32::NEG_INFINITY, Color::RED).position, 0.0);
        assert_eq!(GradientStop::new(-0.5, Color::RED).position, 0.0);
        assert_eq!(GradientStop::new(1.5, Color::RED).position, 1.0);
        assert_eq!(GradientStop::new(0.25, Color::RED).position, 0.25);
    }

    /// A stop built through the public field with `NaN` is normalised by `with_stops`, so the
    /// range contract and the sort hold even for a directly-constructed stop.
    #[test]
    fn with_stops_normalizes_directly_constructed_stop_positions() {
        let stops = vec![
            GradientStop { position: f32::NAN, color: Color::RED },
            GradientStop { position: f32::INFINITY, color: Color::BLUE },
            GradientStop { position: -1.0, color: Color::GREEN },
        ];
        let g = Gradient::linear(Point::new(0, 0), Point::new(100, 0)).with_stops(stops);
        let positions: Vec<f32> = g.stops.iter().map(|s| s.position).collect();
        assert!(
            positions.iter().all(|p| p.is_finite() && (0.0..=1.0).contains(p)),
            "every stored position must be finite and in range: {positions:?}"
        );
        // NaN -> 0.0 and -1.0 -> 0.0 tie at the start; +∞ -> 1.0 is last, so the ramp is sorted.
        assert_eq!(g.stops[g.stops.len() - 1].position, 1.0);
    }

    /// `interpolate(NaN)` must read as a defined value (the ramp start) rather than falling
    /// through to the last stop's colour.
    #[test]
    fn interpolate_nan_reads_as_the_ramp_start() {
        let g = Gradient::linear(Point::new(0, 0), Point::new(100, 0))
            .add_stop(0.0, Color::BLACK)
            .add_stop(1.0, Color::WHITE);
        assert_eq!(g.interpolate(f32::NAN), Color::BLACK);
        assert_eq!(g.interpolate(f32::NEG_INFINITY), Color::BLACK);
        assert_eq!(g.interpolate(f32::INFINITY), Color::WHITE);
    }

    /// The builder path routes through `GradientStop::new`, so a non-finite stop gives the same
    /// defined position as the standalone constructor (D09-STYLE-03).
    #[test]
    fn gradient_builder_normalizes_non_finite_stop_positions() {
        let g = GradientBuilder::linear(Point::new(0, 0), Point::new(100, 0))
            .stop(f32::NAN, Color::RED)
            .stop(f32::INFINITY, Color::BLUE)
            .build();
        assert!(g.stops.iter().all(|s| s.position.is_finite()));
        assert_eq!(g.stops[0].position, 0.0);
        assert_eq!(g.stops[g.stops.len() - 1].position, 1.0);
    }
}