livesplit_core/rendering/
mod.rs

1//! The rendering module provides a [`SceneManager`], that when provided with a
2//! [`LayoutState`], places [`Entities`](Entity) into a [`Scene`] and updates it
3//! accordingly as the [`LayoutState`] changes. It is up to a specific renderer
4//! to then take the [`Scene`] and render out the [`Entities`](Entity). There is
5//! a [`ResourceAllocator`] trait that defines the types of resources an
6//! [`Entity`] consists of. A specific renderer can then provide an
7//! implementation that provides the resources it can render out. Those
8//! resources are images, paths, i.e. lines, quadratic and cubic bezier curves,
9//! fonts and labels. An optional software renderer is available behind the
10//! `software-rendering` feature that uses tiny-skia to efficiently render the
11//! paths on the CPU. It is surprisingly fast and can be considered the default
12//! renderer.
13
14// # Coordinate spaces used in this module
15//
16// ## Backend Coordinate Space
17//
18// The backend can choose its own coordinate space by passing its own width and
19// height to the renderer. (0, 0) is the top left corner of the rendered layout,
20// and (width, height) is the bottom right corner. In most situations width and
21// height will be the actual pixel dimensions of the image that is being
22// rendered to.
23//
24// ## Renderer Coordinate Space
25//
26// The renderer internally uses the so called renderer coordinate space. It has
27// the dimensions [width, 1] with the width being chosen such that the renderer
28// coordinate space respects the aspect ratio of the render target. This
29// coordinate space is mostly an implementation detail.
30//
31// ## Component Coordinate Space
32//
33// The component coordinate space is a coordinate space local to a single
34// component. This means that (0, 0) is the top left corner and (width, height)
35// is the bottom right corner of the component. Width and Height are chosen
36// based on various properties. In vertical mode, the height is chosen to be the
37// component's actual height, while the width is dynamically adjusted based on
38// the other components in the layout and the dimensions of the render target.
39// In horizontal mode, the height is always the two row height, while the width
40// is dynamically adjusted based the component's width preference. The width
41// preference however only serves as a ratio of how much of the total width to
42// distribute to the individual components. So similar to vertical mode, the
43// width is fairly dynamic.
44//
45// ## Default Pixel Space
46//
47// The default pixel space describes a default scaling factor to apply to the
48// component coordinate space. Both the original LiveSplit as well as
49// livesplit-core internally use this coordinate space to store the component
50// settings that influence dimensions of elements drawn on the component, such
51// as font sizes and the dimensions of the component itself. It also serves as a
52// good default size when choosing the size of a window or an image when the
53// preferred size of the layout is unknown. The factor for converting component
54// space coordinates to the default pixel space coordinates is 24.
55//
56// ### Guidelines for Spacing and Sizes in the Component Coordinate Space
57//
58// The default height of a component in the component coordinate space is 1.
59// This is equal to the height of one split or one key value component. The
60// default text size is 0.725. There is a padding of 0.35 to the left and right
61// side of a component for the contents shown inside a component, such as images
62// and texts. The same padding of 0.35 is also used for the minimum spacing
63// between text and other content such as an icon or another text. A vertical
64// padding of 10% of the height of the available space is chosen for images
65// unless that is larger than the normal padding. If text doesn't fit, it is to
66// be either abbreviated or overflown via the use of ellipsis. Numbers and times
67// are supposed to be aligned to the right and should be using a monospace text
68// layout. Sometimes components are rendered in two row mode. The height of
69// these components is 1.725. All components also need to be able to render with
70// this height in horizontal mode. Separators have a thickness of 0.1, while
71// thin separators have half of this thickness.
72
73mod component;
74mod consts;
75mod entity;
76mod font;
77mod icon;
78mod resource;
79mod scene;
80
81#[cfg(feature = "path-based-text-engine")]
82pub mod path_based_text_engine;
83
84#[cfg(feature = "software-rendering")]
85pub mod software;
86
87use self::{
88    consts::{
89        DEFAULT_TEXT_SIZE, DEFAULT_VERTICAL_WIDTH, PADDING, TEXT_ALIGN_BOTTOM, TEXT_ALIGN_TOP,
90        TWO_ROW_HEIGHT,
91    },
92    font::{AbbreviatedLabel, CachedLabel, FontCache},
93    icon::Icon,
94    resource::Handles,
95};
96use crate::{
97    layout::{LayoutDirection, LayoutState},
98    platform::prelude::*,
99    settings::{Color, Gradient},
100};
101use alloc::borrow::Cow;
102use bytemuck::{Pod, Zeroable};
103use core::iter;
104
105pub use self::{
106    entity::Entity,
107    font::{TEXT_FONT, TIMER_FONT},
108    resource::{
109        FontKind, Handle, Label, LabelHandle, PathBuilder, ResourceAllocator, SharedOwnership,
110    },
111    scene::{Layer, Scene},
112};
113
114/// Describes a coordinate in 2D space.
115pub type Pos = [f32; 2];
116/// A color encoded as RGBA (red, green, blue, alpha) where each component is
117/// stored as a value between 0 and 1.
118pub type Rgba = [f32; 4];
119
120/// A transformation to apply to the entities in order to place them into the
121/// scene.
122#[derive(Copy, Clone, Pod, Zeroable)]
123#[repr(C)]
124pub struct Transform {
125    /// Scale the x coordinate by this value.
126    pub scale_x: f32,
127    /// Scale the y coordinate by this value.
128    pub scale_y: f32,
129    /// Add this value to the x coordinate after scaling it.
130    pub x: f32,
131    /// Add this value to the y coordinate after scaling it.
132    pub y: f32,
133}
134
135/// Specifies the colors to use when filling a path.
136#[derive(Copy, Clone, PartialEq)]
137pub enum FillShader {
138    /// Use a single color for the whole path.
139    SolidColor(Rgba),
140    /// Use a vertical gradient (top, bottom) to fill the path.
141    VerticalGradient(Rgba, Rgba),
142    /// Use a horizontal gradient (left, right) to fill the path.
143    HorizontalGradient(Rgba, Rgba),
144}
145
146enum CachedSize {
147    Vertical(f32),
148    Horizontal(f32),
149}
150
151/// The scene manager is the main entry point when it comes to writing a
152/// renderer for livesplit-core. When provided with a [`LayoutState`], it places
153/// [`Entities`](Entity) into a [`Scene`] and updates it accordingly as the
154/// [`LayoutState`] changes. It is up to a specific renderer to then take the
155/// [`Scene`] and render out the [`Entities`](Entity). There is a
156/// [`ResourceAllocator`] trait that defines the types of resources an
157/// [`Entity`] consists of. A specific renderer can then provide an
158/// implementation that provides the resources it can render out. Those
159/// resources are images, paths, i.e. lines, quadratic and cubic bezier
160/// curves, fonts and labels.
161pub struct SceneManager<P, I, F, L> {
162    scene: Scene<P, I, L>,
163    components: Vec<component::Cache<I, L>>,
164    next_id: usize,
165    cached_size: Option<CachedSize>,
166    fonts: FontCache<F>,
167}
168
169impl<P: SharedOwnership, I: SharedOwnership, F, L: SharedOwnership> SceneManager<P, I, F, L> {
170    /// Creates a new scene manager.
171    pub fn new(
172        mut allocator: impl ResourceAllocator<Path = P, Image = I, Font = F, Label = L>,
173    ) -> Self {
174        let mut builder = allocator.path_builder();
175        builder.move_to(0.0, 0.0);
176        builder.line_to(0.0, 1.0);
177        builder.line_to(1.0, 1.0);
178        builder.line_to(1.0, 0.0);
179        builder.close();
180        let rectangle = Handle::new(0, builder.finish());
181
182        let mut handles = Handles::new(1, allocator);
183        let fonts = FontCache::new(&mut handles);
184
185        Self {
186            components: Vec::new(),
187            next_id: handles.into_next_id(),
188            scene: Scene::new(rectangle),
189            cached_size: None,
190            fonts,
191        }
192    }
193
194    /// Accesses the [`Scene`] in order to render the [`Entities`](Entity).
195    pub const fn scene(&self) -> &Scene<P, I, L> {
196        &self.scene
197    }
198
199    /// Updates the [`Scene`] by updating the [`Entities`](Entity) according to
200    /// the [`LayoutState`] provided. The [`ResourceAllocator`] is used to
201    /// allocate the resources necessary that the [`Entities`](Entity) use. A
202    /// resolution needs to be provided as well so that the [`Entities`](Entity)
203    /// are positioned and sized correctly for a renderer to then consume. If a
204    /// change in the layout size is detected, a new more suitable resolution
205    /// for subsequent updates is being returned. This is however merely a hint
206    /// and can be completely ignored.
207    pub fn update_scene<A: ResourceAllocator<Path = P, Image = I, Font = F, Label = L>>(
208        &mut self,
209        allocator: A,
210        resolution: (f32, f32),
211        state: &LayoutState,
212    ) -> Option<(f32, f32)> {
213        self.scene.clear();
214
215        self.scene
216            .set_background(decode_gradient(&state.background));
217
218        // Ensure we have exactly as many cached components as the layout state.
219        if let Some(new_components) = state.components.get(self.components.len()..) {
220            self.components
221                .extend(new_components.iter().map(component::Cache::new));
222        } else {
223            self.components.truncate(state.components.len());
224        }
225
226        let new_dimensions = match state.direction {
227            LayoutDirection::Vertical => self.render_vertical(allocator, resolution, state),
228            LayoutDirection::Horizontal => self.render_horizontal(allocator, resolution, state),
229        };
230
231        self.scene.recalculate_if_bottom_layer_changed();
232
233        new_dimensions
234    }
235
236    fn render_vertical(
237        &mut self,
238        allocator: impl ResourceAllocator<Path = P, Image = I, Font = F, Label = L>,
239        resolution: (f32, f32),
240        state: &LayoutState,
241    ) -> Option<(f32, f32)> {
242        let total_height = component::layout_height(state);
243
244        let cached_total_size = self
245            .cached_size
246            .get_or_insert(CachedSize::Vertical(total_height));
247        let mut new_resolution = None;
248
249        match cached_total_size {
250            CachedSize::Vertical(cached_total_height) => {
251                if cached_total_height.to_bits() != total_height.to_bits() {
252                    new_resolution = Some((
253                        resolution.0,
254                        resolution.1 / *cached_total_height * total_height,
255                    ));
256                    *cached_total_height = total_height;
257                }
258            }
259            CachedSize::Horizontal(_) => {
260                let to_pixels = resolution.1 / TWO_ROW_HEIGHT;
261                let new_height = total_height * to_pixels;
262                let new_width = DEFAULT_VERTICAL_WIDTH * to_pixels;
263                new_resolution = Some((new_width, new_height));
264                *cached_total_size = CachedSize::Vertical(total_height);
265            }
266        }
267
268        let aspect_ratio = resolution.0 / resolution.1;
269
270        let mut context = RenderContext {
271            handles: Handles::new(self.next_id, allocator),
272            transform: Transform::scale(resolution.0, resolution.1),
273            scene: &mut self.scene,
274            fonts: &mut self.fonts,
275        };
276
277        context.fonts.maybe_reload(&mut context.handles, state);
278
279        // Now we transform the coordinate space to Renderer Coordinate Space by
280        // non-uniformly adjusting for the aspect ratio.
281        context.scale_non_uniform_x(aspect_ratio.recip());
282
283        // We scale the coordinate space uniformly such that we have the same
284        // scaling as the Component Coordinate Space. This also already is the
285        // Component Coordinate Space for the component at (0, 0).
286        context.scale(total_height.recip());
287
288        // Calculate the width of the components in component space. In vertical
289        // mode, all the components have the same width.
290        let width = aspect_ratio * total_height;
291
292        for (component, cache) in state.components.iter().zip(&mut self.components) {
293            let height = component::height(component);
294            let dim = [width, height];
295            component::render(cache, &mut context, component, state, dim);
296            // We translate the coordinate space to the Component Coordinate
297            // Space of the next component by shifting by the height of the
298            // current component in the Component Coordinate Space.
299            context.translate(0.0, height);
300        }
301
302        self.next_id = context.handles.into_next_id();
303
304        new_resolution
305    }
306
307    fn render_horizontal(
308        &mut self,
309        allocator: impl ResourceAllocator<Path = P, Image = I, Font = F, Label = L>,
310        resolution: (f32, f32),
311        state: &LayoutState,
312    ) -> Option<(f32, f32)> {
313        let total_width = component::layout_width(state);
314
315        let cached_total_size = self
316            .cached_size
317            .get_or_insert(CachedSize::Horizontal(total_width));
318        let mut new_resolution = None;
319
320        match cached_total_size {
321            CachedSize::Vertical(cached_total_height) => {
322                let new_height = resolution.1 * TWO_ROW_HEIGHT / *cached_total_height;
323                let new_width = total_width * new_height / TWO_ROW_HEIGHT;
324                new_resolution = Some((new_width, new_height));
325                *cached_total_size = CachedSize::Horizontal(total_width);
326            }
327            CachedSize::Horizontal(cached_total_width) => {
328                if cached_total_width.to_bits() != total_width.to_bits() {
329                    new_resolution = Some((
330                        resolution.0 / *cached_total_width * total_width,
331                        resolution.1,
332                    ));
333                    *cached_total_width = total_width;
334                }
335            }
336        }
337
338        let aspect_ratio = resolution.0 / resolution.1;
339
340        let mut context = RenderContext {
341            handles: Handles::new(self.next_id, allocator),
342            transform: Transform::scale(resolution.0, resolution.1),
343            scene: &mut self.scene,
344            fonts: &mut self.fonts,
345        };
346
347        context.fonts.maybe_reload(&mut context.handles, state);
348
349        // Now we transform the coordinate space to Renderer Coordinate Space by
350        // non-uniformly adjusting for the aspect ratio.
351        context.scale_non_uniform_x(aspect_ratio.recip());
352
353        // We scale the coordinate space uniformly such that we have the same
354        // scaling as the Component Coordinate Space. This also already is the
355        // Component Coordinate Space for the component at (0, 0). Since all the
356        // components use the two row height as their height, we scale by the
357        // reciprocal of that.
358        context.scale(TWO_ROW_HEIGHT.recip());
359
360        // We don't take the component width we calculate. Instead we use the
361        // component width as a ratio of how much of the total actual width to
362        // distribute to each of the components. This factor is this adjustment.
363        let width_scaling = TWO_ROW_HEIGHT * aspect_ratio / total_width;
364
365        for (component, cache) in state.components.iter().zip(&mut self.components) {
366            let width = component::width(component) * width_scaling;
367            let height = TWO_ROW_HEIGHT;
368            let dim = [width, height];
369            component::render(cache, &mut context, component, state, dim);
370            // We translate the coordinate space to the Component Coordinate
371            // Space of the next component by shifting by the width of the
372            // current component in the Component Coordinate Space.
373            context.translate(width, 0.0);
374        }
375
376        self.next_id = context.handles.into_next_id();
377
378        new_resolution
379    }
380}
381
382struct RenderContext<'b, A: ResourceAllocator> {
383    transform: Transform,
384    handles: Handles<A>,
385    scene: &'b mut Scene<A::Path, A::Image, A::Label>,
386    fonts: &'b mut FontCache<A::Font>,
387}
388
389impl<A: ResourceAllocator> RenderContext<'_, A> {
390    fn rectangle(&self) -> Handle<A::Path> {
391        self.scene.rectangle()
392    }
393
394    fn render_background(&mut self, [w, h]: Pos, gradient: &Gradient) {
395        if let Some(shader) = decode_gradient(gradient) {
396            let rectangle = self.rectangle();
397            self.scene.bottom_layer_mut().push(Entity::FillPath(
398                rectangle,
399                shader,
400                self.transform.pre_scale(w, h),
401            ));
402        }
403    }
404
405    fn backend_render_rectangle(&mut self, [x1, y1]: Pos, [x2, y2]: Pos, shader: FillShader) {
406        let transform = self
407            .transform
408            .pre_translate(x1, y1)
409            .pre_scale(x2 - x1, y2 - y1);
410
411        let rectangle = self.rectangle();
412
413        self.scene
414            .bottom_layer_mut()
415            .push(Entity::FillPath(rectangle, shader, transform));
416    }
417
418    fn backend_render_top_rectangle(&mut self, [x1, y1]: Pos, [x2, y2]: Pos, shader: FillShader) {
419        let transform = self
420            .transform
421            .pre_translate(x1, y1)
422            .pre_scale(x2 - x1, y2 - y1);
423
424        let rectangle = self.rectangle();
425
426        self.scene
427            .top_layer_mut()
428            .push(Entity::FillPath(rectangle, shader, transform));
429    }
430
431    fn top_layer_path(&mut self, path: Handle<A::Path>, color: Color) {
432        self.scene
433            .top_layer_mut()
434            .push(Entity::FillPath(path, solid(&color), self.transform));
435    }
436
437    fn top_layer_stroke_path(&mut self, path: Handle<A::Path>, color: Color, stroke_width: f32) {
438        self.scene.top_layer_mut().push(Entity::StrokePath(
439            path,
440            stroke_width,
441            color.to_array(),
442            self.transform,
443        ));
444    }
445
446    fn create_icon(&mut self, image_data: &[u8]) -> Option<Icon<A::Image>> {
447        let (image, aspect_ratio) = self.handles.create_image(image_data)?;
448        Some(Icon {
449            aspect_ratio,
450            image,
451        })
452    }
453
454    fn scale(&mut self, factor: f32) {
455        self.transform = self.transform.pre_scale(factor, factor);
456    }
457
458    fn scale_non_uniform_x(&mut self, x: f32) {
459        self.transform = self.transform.pre_scale(x, 1.0);
460    }
461
462    fn translate(&mut self, x: f32, y: f32) {
463        self.transform = self.transform.pre_translate(x, y);
464    }
465
466    fn render_rectangle(&mut self, top_left: Pos, bottom_right: Pos, gradient: &Gradient) {
467        if let Some(colors) = decode_gradient(gradient) {
468            self.backend_render_rectangle(top_left, bottom_right, colors);
469        }
470    }
471
472    fn render_top_rectangle(&mut self, top_left: Pos, bottom_right: Pos, gradient: &Gradient) {
473        if let Some(colors) = decode_gradient(gradient) {
474            self.backend_render_top_rectangle(top_left, bottom_right, colors);
475        }
476    }
477
478    fn render_icon(
479        &mut self,
480        [mut x, mut y]: Pos,
481        [mut width, mut height]: Pos,
482        icon: &Icon<A::Image>,
483    ) {
484        let box_aspect_ratio = width / height;
485        let aspect_ratio_diff = box_aspect_ratio / icon.aspect_ratio;
486
487        if aspect_ratio_diff > 1.0 {
488            let new_width = width / aspect_ratio_diff;
489            let diff_width = width - new_width;
490            x += 0.5 * diff_width;
491            width = new_width;
492        } else if aspect_ratio_diff < 1.0 {
493            let new_height = height * aspect_ratio_diff;
494            let diff_height = height - new_height;
495            y += 0.5 * diff_height;
496            height = new_height;
497        }
498
499        let transform = self.transform.pre_translate(x, y).pre_scale(width, height);
500
501        self.scene
502            .bottom_layer_mut()
503            .push(Entity::Image(icon.image.share(), transform));
504    }
505
506    fn render_key_value_component(
507        &mut self,
508        key: &str,
509        abbreviations: &[Cow<'_, str>],
510        key_label: &mut AbbreviatedLabel<A::Label>,
511        value: &str,
512        value_label: &mut CachedLabel<A::Label>,
513        updates_frequently: bool,
514        [width, height]: [f32; 2],
515        key_color: Color,
516        value_color: Color,
517        display_two_rows: bool,
518    ) {
519        let left_of_value_x = self.render_numbers(
520            value,
521            value_label,
522            Layer::from_updates_frequently(updates_frequently),
523            [width - PADDING, height + TEXT_ALIGN_BOTTOM],
524            DEFAULT_TEXT_SIZE,
525            solid(&value_color),
526        );
527        let end_x = if display_two_rows {
528            width
529        } else {
530            left_of_value_x
531        };
532
533        self.render_abbreviated_text_ellipsis(
534            iter::once(key).chain(abbreviations.iter().map(|x| &**x)),
535            key_label,
536            [PADDING, TEXT_ALIGN_TOP],
537            DEFAULT_TEXT_SIZE,
538            solid(&key_color),
539            end_x - PADDING,
540        );
541    }
542
543    fn render_abbreviated_text_ellipsis<'a>(
544        &mut self,
545        abbreviations: impl IntoIterator<Item = &'a str> + Clone,
546        label: &mut AbbreviatedLabel<A::Label>,
547        pos @ [x, _]: Pos,
548        scale: f32,
549        shader: FillShader,
550        max_x: f32,
551    ) -> f32 {
552        let label = label.update(
553            abbreviations,
554            &mut self.handles,
555            &mut self.fonts.text.font,
556            (max_x - x) / scale,
557        );
558
559        self.scene.bottom_layer_mut().push(Entity::Label(
560            label.share(),
561            shader,
562            font::left_aligned(&self.transform, pos, scale),
563        ));
564
565        x + label.width(scale)
566    }
567
568    fn render_text_ellipsis(
569        &mut self,
570        text: &str,
571        label: &mut CachedLabel<A::Label>,
572        pos @ [x, _]: Pos,
573        scale: f32,
574        shader: FillShader,
575        max_x: f32,
576    ) -> f32 {
577        let label = label.update(
578            text,
579            &mut self.handles,
580            &mut self.fonts.text.font,
581            Some((max_x - x) / scale),
582        );
583
584        self.scene.bottom_layer_mut().push(Entity::Label(
585            label.share(),
586            shader,
587            font::left_aligned(&self.transform, pos, scale),
588        ));
589
590        x + label.width(scale)
591    }
592
593    fn render_text_centered(
594        &mut self,
595        text: &str,
596        label: &mut CachedLabel<A::Label>,
597        min_x: f32,
598        max_x: f32,
599        pos: Pos,
600        scale: f32,
601        shader: FillShader,
602    ) {
603        let label = label.update(
604            text,
605            &mut self.handles,
606            &mut self.fonts.text.font,
607            Some((max_x - min_x) / scale),
608        );
609
610        self.scene.bottom_layer_mut().push(Entity::Label(
611            label.share(),
612            shader,
613            font::centered(
614                &self.transform,
615                pos,
616                scale,
617                label.width(scale),
618                min_x,
619                max_x,
620            ),
621        ));
622    }
623
624    fn render_abbreviated_text_centered<'a>(
625        &mut self,
626        abbreviations: impl IntoIterator<Item = &'a str> + Clone,
627        label: &mut AbbreviatedLabel<A::Label>,
628        min_x: f32,
629        max_x: f32,
630        pos: Pos,
631        scale: f32,
632        shader: FillShader,
633    ) {
634        let label = label.update(
635            abbreviations,
636            &mut self.handles,
637            &mut self.fonts.text.font,
638            (max_x - min_x) / scale,
639        );
640
641        self.scene.bottom_layer_mut().push(Entity::Label(
642            label.share(),
643            shader,
644            font::centered(
645                &self.transform,
646                pos,
647                scale,
648                label.width(scale),
649                min_x,
650                max_x,
651            ),
652        ));
653    }
654
655    fn render_text_right_align(
656        &mut self,
657        text: &str,
658        label: &mut CachedLabel<A::Label>,
659        layer: Layer,
660        pos @ [x, _]: Pos,
661        scale: f32,
662        shader: FillShader,
663    ) -> f32 {
664        let label = label.update(text, &mut self.handles, &mut self.fonts.text.font, None);
665        let width = label.width(scale);
666
667        self.scene.layer_mut(layer).push(Entity::Label(
668            label.share(),
669            shader,
670            font::right_aligned(&self.transform, pos, scale, width),
671        ));
672
673        x - width
674    }
675
676    fn render_abbreviated_text_align<'a>(
677        &mut self,
678        abbreviations: impl IntoIterator<Item = &'a str> + Clone,
679        label: &mut AbbreviatedLabel<A::Label>,
680        min_x: f32,
681        max_x: f32,
682        pos: Pos,
683        scale: f32,
684        centered: bool,
685        shader: FillShader,
686    ) {
687        if centered {
688            self.render_abbreviated_text_centered(
689                abbreviations,
690                label,
691                min_x,
692                max_x,
693                pos,
694                scale,
695                shader,
696            );
697        } else {
698            self.render_abbreviated_text_ellipsis(abbreviations, label, pos, scale, shader, max_x);
699        }
700    }
701
702    fn render_numbers(
703        &mut self,
704        text: &str,
705        label: &mut CachedLabel<A::Label>,
706        layer: Layer,
707        pos @ [x, _]: Pos,
708        scale: f32,
709        shader: FillShader,
710    ) -> f32 {
711        let label = label.update(text, &mut self.handles, &mut self.fonts.times.font, None);
712        let width = label.width(scale);
713
714        self.scene.layer_mut(layer).push(Entity::Label(
715            label.share(),
716            shader,
717            font::right_aligned(&self.transform, pos, scale, width),
718        ));
719
720        x - width
721    }
722
723    fn render_timer(
724        &mut self,
725        text: &str,
726        label: &mut CachedLabel<A::Label>,
727        layer: Layer,
728        pos @ [x, _]: Pos,
729        scale: f32,
730        shader: FillShader,
731    ) -> f32 {
732        let label = label.update(text, &mut self.handles, &mut self.fonts.timer.font, None);
733        let width = label.width(scale);
734
735        self.scene.layer_mut(layer).push(Entity::Label(
736            label.share(),
737            shader,
738            font::right_aligned(&self.transform, pos, scale, width),
739        ));
740
741        x - width
742    }
743
744    fn measure_numbers(
745        &mut self,
746        text: &str,
747        label: &mut CachedLabel<A::Label>,
748        scale: f32,
749    ) -> f32 {
750        let label = label.update(text, &mut self.handles, &mut self.fonts.times.font, None);
751        label.width(scale)
752    }
753}
754
755const fn decode_gradient(gradient: &Gradient) -> Option<FillShader> {
756    Some(match gradient {
757        Gradient::Transparent => return None,
758        Gradient::Horizontal(left, right) => {
759            FillShader::HorizontalGradient(left.to_array(), right.to_array())
760        }
761        Gradient::Vertical(top, bottom) => {
762            FillShader::VerticalGradient(top.to_array(), bottom.to_array())
763        }
764        Gradient::Plain(plain) => FillShader::SolidColor(plain.to_array()),
765    })
766}
767
768const fn solid(color: &Color) -> FillShader {
769    FillShader::SolidColor(color.to_array())
770}
771
772impl Transform {
773    const fn scale(scale_x: f32, scale_y: f32) -> Transform {
774        Self {
775            x: 0.0,
776            y: 0.0,
777            scale_x,
778            scale_y,
779        }
780    }
781
782    /// Returns a new transform that first scales the coordinates.
783    pub fn pre_scale(&self, scale_x: f32, scale_y: f32) -> Transform {
784        Self {
785            scale_x: self.scale_x * scale_x,
786            scale_y: self.scale_y * scale_y,
787            x: self.x,
788            y: self.y,
789        }
790    }
791
792    /// Returns a new transform that first translates the coordinates.
793    pub fn pre_translate(&self, x: f32, y: f32) -> Transform {
794        Self {
795            scale_x: self.scale_x,
796            scale_y: self.scale_y,
797            x: self.x + self.scale_x * x,
798            y: self.y + self.scale_y * y,
799        }
800    }
801
802    #[cfg(feature = "software-rendering")]
803    fn transform_y(&self, y: f32) -> f32 {
804        self.y + self.scale_y * y
805    }
806}