Skip to main content

simple_render/ui/render/
paint.rs

1use super::*;
2
3pub(super) fn multiply_opacity(parent: f32, child: f32) -> f32 {
4    (parent * child).clamp(0.0, 1.0)
5}
6
7pub(super) fn border_widths(border: &Border) -> BorderWidth {
8    if border.widths.is_zero() {
9        BorderWidth::all(border.width)
10    } else {
11        border.widths
12    }
13}
14
15pub(super) fn element_needs_measure(element: &Rect) -> bool {
16    matches!(element.width, Length::Fit) || matches!(element.height, Length::Fit)
17}
18
19#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
20pub(super) struct PaintOffset {
21    pub(super) x: i32,
22    pub(super) y: i32,
23}
24
25#[derive(Clone, Copy, Debug, PartialEq)]
26pub struct PaintTransform {
27    pub scale: f32,
28    pub translate_x: i32,
29    pub translate_y: i32,
30}
31
32impl PaintTransform {
33    pub const IDENTITY: Self = Self {
34        scale: 1.0,
35        translate_x: 0,
36        translate_y: 0,
37    };
38
39    pub const fn new(scale: f32, translate_x: i32, translate_y: i32) -> Self {
40        Self {
41            scale,
42            translate_x,
43            translate_y,
44        }
45    }
46
47    pub const fn scale(scale: f32) -> Self {
48        Self {
49            scale,
50            ..Self::IDENTITY
51        }
52    }
53
54    pub const fn translate(translate_x: i32, translate_y: i32) -> Self {
55        Self {
56            translate_x,
57            translate_y,
58            ..Self::IDENTITY
59        }
60    }
61
62    pub(super) fn is_identity(self) -> bool {
63        self.scale == 1.0 && self.translate_x == 0 && self.translate_y == 0
64    }
65}
66
67impl Default for PaintTransform {
68    fn default() -> Self {
69        Self::IDENTITY
70    }
71}
72
73pub(super) fn scale_i32(value: i32, scale: u32) -> i32 {
74    let scaled = i64::from(value) * i64::from(scale);
75    scaled.clamp(i64::from(i32::MIN), i64::from(i32::MAX)) as i32
76}
77
78pub(super) fn scale_i32_f32(value: i32, scale: f32) -> i32 {
79    let scaled = f64::from(value) * f64::from(scale);
80    if !scaled.is_finite() {
81        return if scaled.is_sign_negative() {
82            i32::MIN
83        } else {
84            i32::MAX
85        };
86    }
87    scaled
88        .round()
89        .clamp(f64::from(i32::MIN), f64::from(i32::MAX)) as i32
90}
91
92fn paint_command_with_offset(
93    canvas: &mut Canvas<'_>,
94    fonts: &mut FontCtx,
95    command: PaintCommand<'_>,
96    offset: PaintOffset,
97) {
98    match command {
99        PaintCommand::Rect {
100            rect,
101            clip,
102            opacity,
103            paint,
104            gradient,
105            radii,
106        } => fill_rounded_rect(canvas, rect, clip, opacity, radii, paint, gradient, offset),
107        PaintCommand::Border {
108            rect,
109            clip,
110            opacity,
111            paint,
112            gradient,
113            widths,
114            radii,
115        } => stroke_rounded_rect(
116            canvas, rect, clip, opacity, widths, radii, paint, gradient, offset,
117        ),
118        PaintCommand::Text {
119            rect,
120            clip,
121            opacity,
122            text,
123        } => draw_text(canvas, fonts, rect, clip, opacity, text, offset),
124        PaintCommand::RichText {
125            rect,
126            clip,
127            opacity,
128            text,
129        } => draw_rich_text(canvas, fonts, rect, clip, opacity, text, offset),
130        PaintCommand::Image {
131            rect,
132            clip,
133            opacity,
134            image,
135        } => draw_image(canvas, rect, clip, opacity, image, offset),
136    }
137}
138
139fn normalized_opacity(opacity: f32) -> f32 {
140    opacity.clamp(0.0, 1.0)
141}
142
143pub(super) fn opacity_draws(opacity: f32) -> bool {
144    normalized_opacity(opacity) > 0.0
145}
146
147pub(super) fn color_with_opacity_and_coverage(color: Color, opacity: f32, coverage: u8) -> [u8; 4] {
148    let opacity = normalized_opacity(opacity);
149    if opacity >= 1.0 && coverage == 255 {
150        return color.into();
151    }
152
153    let alpha = (f32::from(color.alpha) * opacity * (f32::from(coverage) / 255.0)).round() as u8;
154    Color { alpha, ..color }.into()
155}
156
157pub(super) fn pixel_with_opacity_and_coverage(
158    mut rgba: [u8; 4],
159    opacity: f32,
160    coverage: u8,
161) -> [u8; 4] {
162    let opacity = normalized_opacity(opacity);
163    if opacity < 1.0 || coverage != 255 {
164        rgba[3] = (f32::from(rgba[3]) * opacity * (f32::from(coverage) / 255.0)).round() as u8;
165    }
166    rgba
167}
168
169fn visible_world_bounds(canvas: &Canvas<'_>, offset: PaintOffset) -> Option<Bounds> {
170    let left = i64::from(offset.x).max(0);
171    let top = i64::from(offset.y).max(0);
172    let right = i64::from(offset.x).saturating_add(i64::from(canvas.width()));
173    let bottom = i64::from(offset.y).saturating_add(i64::from(canvas.height()));
174    if right <= left || bottom <= top {
175        return None;
176    }
177
178    Some(Bounds {
179        x: left.min(i64::from(u32::MAX)) as u32,
180        y: top.min(i64::from(u32::MAX)) as u32,
181        width: right.min(i64::from(u32::MAX)).saturating_sub(left).max(0) as u32,
182        height: bottom.min(i64::from(u32::MAX)).saturating_sub(top).max(0) as u32,
183    })
184}
185
186pub(super) fn visible_draw_bounds(
187    canvas: &Canvas<'_>,
188    bounds: Bounds,
189    offset: PaintOffset,
190) -> Option<Bounds> {
191    bounds.intersect(visible_world_bounds(canvas, offset)?)
192}
193
194pub(super) fn target_coord(world: u32, offset: i32, max: u32) -> Option<u32> {
195    let target = i64::from(world) - i64::from(offset);
196    (target >= 0 && target < i64::from(max)).then_some(target as u32)
197}
198
199pub(super) fn paint_scaled_command_with_offset(
200    canvas: &mut Canvas<'_>,
201    fonts: &mut FontCtx,
202    command: PaintCommand<'_>,
203    scale: u32,
204    offset: PaintOffset,
205) {
206    if scale == 1 {
207        paint_command_with_offset(canvas, fonts, command, offset);
208        return;
209    }
210
211    match command {
212        PaintCommand::Rect {
213            rect,
214            clip,
215            opacity,
216            paint,
217            gradient,
218            radii,
219        } => fill_rounded_rect(
220            canvas,
221            scale_bounds(rect, scale),
222            scale_clip(clip, scale),
223            opacity,
224            radii.scaled(scale),
225            paint,
226            gradient,
227            offset,
228        ),
229        PaintCommand::Border {
230            rect,
231            clip,
232            opacity,
233            paint,
234            gradient,
235            widths,
236            radii,
237        } => stroke_rounded_rect(
238            canvas,
239            scale_bounds(rect, scale),
240            scale_clip(clip, scale),
241            opacity,
242            widths.scaled(scale),
243            radii.scaled(scale),
244            paint,
245            gradient,
246            offset,
247        ),
248        PaintCommand::Text {
249            rect,
250            clip,
251            opacity,
252            text,
253        } => {
254            let text = scaled_text(text, scale);
255            draw_text(
256                canvas,
257                fonts,
258                scale_bounds(rect, scale),
259                scale_clip(clip, scale),
260                opacity,
261                &text,
262                offset,
263            );
264        }
265        PaintCommand::RichText {
266            rect,
267            clip,
268            opacity,
269            text,
270        } => {
271            let text = scaled_rich_text(text, scale);
272            draw_rich_text(
273                canvas,
274                fonts,
275                scale_bounds(rect, scale),
276                scale_clip(clip, scale),
277                opacity,
278                &text,
279                offset,
280            );
281        }
282        PaintCommand::Image {
283            rect,
284            clip,
285            opacity,
286            image,
287        } => draw_image(
288            canvas,
289            scale_bounds(rect, scale),
290            scale_clip(clip, scale),
291            opacity,
292            image,
293            offset,
294        ),
295    }
296}
297
298pub(super) fn paint_scaled_f32_command_with_offset(
299    canvas: &mut Canvas<'_>,
300    fonts: &mut FontCtx,
301    command: PaintCommand<'_>,
302    scale: f32,
303    offset: PaintOffset,
304) {
305    if scale == 1.0 {
306        paint_command_with_offset(canvas, fonts, command, offset);
307        return;
308    }
309
310    match command {
311        PaintCommand::Rect {
312            rect,
313            clip,
314            opacity,
315            paint,
316            gradient,
317            radii,
318        } => fill_rounded_rect(
319            canvas,
320            scale_bounds_f32(rect, scale),
321            scale_clip_f32(clip, scale),
322            opacity,
323            scale_corner_radius_f32(radii, scale),
324            paint,
325            gradient,
326            offset,
327        ),
328        PaintCommand::Border {
329            rect,
330            clip,
331            opacity,
332            paint,
333            gradient,
334            widths,
335            radii,
336        } => stroke_rounded_rect(
337            canvas,
338            scale_bounds_f32(rect, scale),
339            scale_clip_f32(clip, scale),
340            opacity,
341            scale_border_width_f32(widths, scale),
342            scale_corner_radius_f32(radii, scale),
343            paint,
344            gradient,
345            offset,
346        ),
347        PaintCommand::Text {
348            rect,
349            clip,
350            opacity,
351            text,
352        } => {
353            let text = scaled_text_f32(text, scale);
354            draw_text(
355                canvas,
356                fonts,
357                scale_bounds_f32(rect, scale),
358                scale_clip_f32(clip, scale),
359                opacity,
360                &text,
361                offset,
362            );
363        }
364        PaintCommand::RichText {
365            rect,
366            clip,
367            opacity,
368            text,
369        } => {
370            let text = scaled_rich_text_f32(text, scale);
371            draw_rich_text(
372                canvas,
373                fonts,
374                scale_bounds_f32(rect, scale),
375                scale_clip_f32(clip, scale),
376                opacity,
377                &text,
378                offset,
379            );
380        }
381        PaintCommand::Image {
382            rect,
383            clip,
384            opacity,
385            image,
386        } => draw_image(
387            canvas,
388            scale_bounds_f32(rect, scale),
389            scale_clip_f32(clip, scale),
390            opacity,
391            image,
392            offset,
393        ),
394    }
395}
396
397fn scale_bounds(bounds: Bounds, scale: u32) -> Bounds {
398    Bounds {
399        x: scale_value(bounds.x, scale),
400        y: scale_value(bounds.y, scale),
401        width: scale_value(bounds.width, scale),
402        height: scale_value(bounds.height, scale),
403    }
404}
405
406fn scale_value(value: u32, scale: u32) -> u32 {
407    value.saturating_mul(scale)
408}
409
410fn scale_clip(clip: Clip, scale: u32) -> Clip {
411    let mut scaled = Clip::rect(scale_bounds(clip.bounds(), scale));
412    for rounded in clip.rounded[..usize::from(clip.rounded_len)].iter() {
413        scaled = scaled
414            .with_rounded_rect(
415                scale_bounds(rounded.rect, scale),
416                rounded.radii.scaled(scale),
417            )
418            .expect("scaled rounded clip remains inside scaled bounds");
419    }
420    scaled
421}
422
423fn scale_bounds_f32(bounds: Bounds, scale: f32) -> Bounds {
424    let x = scale_floor_u32(bounds.x, scale);
425    let y = scale_floor_u32(bounds.y, scale);
426    let right = scale_ceil_u32(bounds.right(), scale);
427    let bottom = scale_ceil_u32(bounds.bottom(), scale);
428    Bounds {
429        x,
430        y,
431        width: right.saturating_sub(x),
432        height: bottom.saturating_sub(y),
433    }
434}
435
436fn scale_value_f32(value: u32, scale: f32) -> u32 {
437    clamp_scaled_u32(f64::from(value) * f64::from(scale), FloatRound::Round)
438}
439
440fn scale_floor_u32(value: u32, scale: f32) -> u32 {
441    clamp_scaled_u32(f64::from(value) * f64::from(scale), FloatRound::Floor)
442}
443
444fn scale_ceil_u32(value: u32, scale: f32) -> u32 {
445    clamp_scaled_u32(f64::from(value) * f64::from(scale), FloatRound::Ceil)
446}
447
448#[derive(Clone, Copy)]
449enum FloatRound {
450    Floor,
451    Round,
452    Ceil,
453}
454
455fn clamp_scaled_u32(value: f64, round: FloatRound) -> u32 {
456    if !value.is_finite() || value <= 0.0 {
457        return 0;
458    }
459    let rounded = match round {
460        FloatRound::Floor => value.floor(),
461        FloatRound::Round => value.round(),
462        FloatRound::Ceil => value.ceil(),
463    };
464    rounded.min(f64::from(u32::MAX)) as u32
465}
466
467fn scale_clip_f32(clip: Clip, scale: f32) -> Clip {
468    let mut scaled = Clip::rect(scale_bounds_f32(clip.bounds(), scale));
469    for rounded in clip.rounded[..usize::from(clip.rounded_len)].iter() {
470        if let Some(next) = scaled.with_rounded_rect(
471            scale_bounds_f32(rounded.rect, scale),
472            scale_corner_radius_f32(rounded.radii, scale),
473        ) {
474            scaled = next;
475        }
476    }
477    scaled
478}
479
480fn scale_corner_radius_f32(radii: CornerRadius, scale: f32) -> CornerRadius {
481    CornerRadius {
482        top_left: scale_value_f32(radii.top_left, scale),
483        top_right: scale_value_f32(radii.top_right, scale),
484        bottom_right: scale_value_f32(radii.bottom_right, scale),
485        bottom_left: scale_value_f32(radii.bottom_left, scale),
486    }
487}
488
489fn scale_border_width_f32(widths: BorderWidth, scale: f32) -> BorderWidth {
490    BorderWidth {
491        top: scale_value_f32(widths.top, scale),
492        right: scale_value_f32(widths.right, scale),
493        bottom: scale_value_f32(widths.bottom, scale),
494        left: scale_value_f32(widths.left, scale),
495    }
496}
497
498pub(super) fn scaled_text(text: &Text, scale: u32) -> Text {
499    let mut text = text.clone();
500    scale_text_style(&mut text.style, scale);
501    text
502}
503
504pub(super) fn scaled_rich_text(text: &RichText, scale: u32) -> RichText {
505    let mut text = text.clone();
506    text.runs = text
507        .runs
508        .iter()
509        .cloned()
510        .map(|mut run| {
511            scale_text_style(&mut run.style, scale);
512            run
513        })
514        .collect();
515    text
516}
517
518fn scale_text_style(style: &mut TextStyle, scale: u32) {
519    style.size = scale_value(style.size, scale);
520}
521
522pub(super) fn scaled_text_f32(text: &Text, scale: f32) -> Text {
523    let mut text = text.clone();
524    scale_text_style_f32(&mut text.style, scale);
525    text
526}
527
528fn scaled_rich_text_f32(text: &RichText, scale: f32) -> RichText {
529    let mut text = text.clone();
530    text.runs = text
531        .runs
532        .iter()
533        .cloned()
534        .map(|mut run| {
535            scale_text_style_f32(&mut run.style, scale);
536            run
537        })
538        .collect();
539    text
540}
541
542fn scale_text_style_f32(style: &mut TextStyle, scale: f32) {
543    style.size = scale_value_f32(style.size, scale).max(1);
544}