mod component;
mod consts;
mod entity;
mod font;
mod icon;
mod resource;
mod scene;
#[cfg(feature = "path-based-text-engine")]
pub mod path_based_text_engine;
#[cfg(feature = "software-rendering")]
pub mod software;
use self::{
consts::{
DEFAULT_TEXT_SIZE, DEFAULT_VERTICAL_WIDTH, PADDING, TEXT_ALIGN_BOTTOM, TEXT_ALIGN_TOP,
TWO_ROW_HEIGHT,
},
font::{AbbreviatedLabel, CachedLabel, FontCache},
icon::Icon,
resource::Handles,
};
use crate::{
layout::{LayoutDirection, LayoutState},
platform::prelude::*,
settings::{Color, Gradient},
};
use alloc::borrow::Cow;
use bytemuck::{Pod, Zeroable};
use core::iter;
pub use self::{
entity::Entity,
font::{TEXT_FONT, TIMER_FONT},
resource::{
FontKind, Handle, Label, LabelHandle, PathBuilder, ResourceAllocator, SharedOwnership,
},
scene::{Layer, Scene},
};
pub type Pos = [f32; 2];
pub type Rgba = [f32; 4];
#[derive(Copy, Clone, Pod, Zeroable)]
#[repr(C)]
pub struct Transform {
pub scale_x: f32,
pub scale_y: f32,
pub x: f32,
pub y: f32,
}
#[derive(Copy, Clone, PartialEq)]
pub enum FillShader {
SolidColor(Rgba),
VerticalGradient(Rgba, Rgba),
HorizontalGradient(Rgba, Rgba),
}
enum CachedSize {
Vertical(f32),
Horizontal(f32),
}
pub struct SceneManager<P, I, F, L> {
scene: Scene<P, I, L>,
components: Vec<component::Cache<I, L>>,
next_id: usize,
cached_size: Option<CachedSize>,
fonts: FontCache<F>,
}
impl<P: SharedOwnership, I: SharedOwnership, F, L: SharedOwnership> SceneManager<P, I, F, L> {
pub fn new(
mut allocator: impl ResourceAllocator<Path = P, Image = I, Font = F, Label = L>,
) -> Self {
let mut builder = allocator.path_builder();
builder.move_to(0.0, 0.0);
builder.line_to(0.0, 1.0);
builder.line_to(1.0, 1.0);
builder.line_to(1.0, 0.0);
builder.close();
let rectangle = Handle::new(0, builder.finish());
let mut handles = Handles::new(1, allocator);
let fonts = FontCache::new(&mut handles);
Self {
components: Vec::new(),
next_id: handles.into_next_id(),
scene: Scene::new(rectangle),
cached_size: None,
fonts,
}
}
pub const fn scene(&self) -> &Scene<P, I, L> {
&self.scene
}
pub fn update_scene<A: ResourceAllocator<Path = P, Image = I, Font = F, Label = L>>(
&mut self,
allocator: A,
resolution: (f32, f32),
state: &LayoutState,
) -> Option<(f32, f32)> {
self.scene.clear();
self.scene
.set_background(decode_gradient(&state.background));
if let Some(new_components) = state.components.get(self.components.len()..) {
self.components
.extend(new_components.iter().map(component::Cache::new));
} else {
self.components.truncate(state.components.len());
}
let new_dimensions = match state.direction {
LayoutDirection::Vertical => self.render_vertical(allocator, resolution, state),
LayoutDirection::Horizontal => self.render_horizontal(allocator, resolution, state),
};
self.scene.recalculate_if_bottom_layer_changed();
new_dimensions
}
fn render_vertical(
&mut self,
allocator: impl ResourceAllocator<Path = P, Image = I, Font = F, Label = L>,
resolution: (f32, f32),
state: &LayoutState,
) -> Option<(f32, f32)> {
let total_height = component::layout_height(state);
let cached_total_size = self
.cached_size
.get_or_insert(CachedSize::Vertical(total_height));
let mut new_resolution = None;
match cached_total_size {
CachedSize::Vertical(cached_total_height) => {
if cached_total_height.to_bits() != total_height.to_bits() {
new_resolution = Some((
resolution.0,
resolution.1 / *cached_total_height * total_height,
));
*cached_total_height = total_height;
}
}
CachedSize::Horizontal(_) => {
let to_pixels = resolution.1 / TWO_ROW_HEIGHT;
let new_height = total_height * to_pixels;
let new_width = DEFAULT_VERTICAL_WIDTH * to_pixels;
new_resolution = Some((new_width, new_height));
*cached_total_size = CachedSize::Vertical(total_height);
}
}
let aspect_ratio = resolution.0 / resolution.1;
let mut context = RenderContext {
handles: Handles::new(self.next_id, allocator),
transform: Transform::scale(resolution.0, resolution.1),
scene: &mut self.scene,
fonts: &mut self.fonts,
};
context.fonts.maybe_reload(&mut context.handles, state);
context.scale_non_uniform_x(aspect_ratio.recip());
context.scale(total_height.recip());
let width = aspect_ratio * total_height;
for (component, cache) in state.components.iter().zip(&mut self.components) {
let height = component::height(component);
let dim = [width, height];
component::render(cache, &mut context, component, state, dim);
context.translate(0.0, height);
}
self.next_id = context.handles.into_next_id();
new_resolution
}
fn render_horizontal(
&mut self,
allocator: impl ResourceAllocator<Path = P, Image = I, Font = F, Label = L>,
resolution: (f32, f32),
state: &LayoutState,
) -> Option<(f32, f32)> {
let total_width = component::layout_width(state);
let cached_total_size = self
.cached_size
.get_or_insert(CachedSize::Horizontal(total_width));
let mut new_resolution = None;
match cached_total_size {
CachedSize::Vertical(cached_total_height) => {
let new_height = resolution.1 * TWO_ROW_HEIGHT / *cached_total_height;
let new_width = total_width * new_height / TWO_ROW_HEIGHT;
new_resolution = Some((new_width, new_height));
*cached_total_size = CachedSize::Horizontal(total_width);
}
CachedSize::Horizontal(cached_total_width) => {
if cached_total_width.to_bits() != total_width.to_bits() {
new_resolution = Some((
resolution.0 / *cached_total_width * total_width,
resolution.1,
));
*cached_total_width = total_width;
}
}
}
let aspect_ratio = resolution.0 / resolution.1;
let mut context = RenderContext {
handles: Handles::new(self.next_id, allocator),
transform: Transform::scale(resolution.0, resolution.1),
scene: &mut self.scene,
fonts: &mut self.fonts,
};
context.fonts.maybe_reload(&mut context.handles, state);
context.scale_non_uniform_x(aspect_ratio.recip());
context.scale(TWO_ROW_HEIGHT.recip());
let width_scaling = TWO_ROW_HEIGHT * aspect_ratio / total_width;
for (component, cache) in state.components.iter().zip(&mut self.components) {
let width = component::width(component) * width_scaling;
let height = TWO_ROW_HEIGHT;
let dim = [width, height];
component::render(cache, &mut context, component, state, dim);
context.translate(width, 0.0);
}
self.next_id = context.handles.into_next_id();
new_resolution
}
}
struct RenderContext<'b, A: ResourceAllocator> {
transform: Transform,
handles: Handles<A>,
scene: &'b mut Scene<A::Path, A::Image, A::Label>,
fonts: &'b mut FontCache<A::Font>,
}
impl<A: ResourceAllocator> RenderContext<'_, A> {
fn rectangle(&self) -> Handle<A::Path> {
self.scene.rectangle()
}
fn render_background(&mut self, [w, h]: Pos, gradient: &Gradient) {
if let Some(shader) = decode_gradient(gradient) {
let rectangle = self.rectangle();
self.scene.bottom_layer_mut().push(Entity::FillPath(
rectangle,
shader,
self.transform.pre_scale(w, h),
));
}
}
fn backend_render_rectangle(&mut self, [x1, y1]: Pos, [x2, y2]: Pos, shader: FillShader) {
let transform = self
.transform
.pre_translate(x1, y1)
.pre_scale(x2 - x1, y2 - y1);
let rectangle = self.rectangle();
self.scene
.bottom_layer_mut()
.push(Entity::FillPath(rectangle, shader, transform));
}
fn backend_render_top_rectangle(&mut self, [x1, y1]: Pos, [x2, y2]: Pos, shader: FillShader) {
let transform = self
.transform
.pre_translate(x1, y1)
.pre_scale(x2 - x1, y2 - y1);
let rectangle = self.rectangle();
self.scene
.top_layer_mut()
.push(Entity::FillPath(rectangle, shader, transform));
}
fn top_layer_path(&mut self, path: Handle<A::Path>, color: Color) {
self.scene
.top_layer_mut()
.push(Entity::FillPath(path, solid(&color), self.transform));
}
fn top_layer_stroke_path(&mut self, path: Handle<A::Path>, color: Color, stroke_width: f32) {
self.scene.top_layer_mut().push(Entity::StrokePath(
path,
stroke_width,
color.to_array(),
self.transform,
));
}
fn create_icon(&mut self, image_data: &[u8]) -> Option<Icon<A::Image>> {
let (image, aspect_ratio) = self.handles.create_image(image_data)?;
Some(Icon {
aspect_ratio,
image,
})
}
fn scale(&mut self, factor: f32) {
self.transform = self.transform.pre_scale(factor, factor);
}
fn scale_non_uniform_x(&mut self, x: f32) {
self.transform = self.transform.pre_scale(x, 1.0);
}
fn translate(&mut self, x: f32, y: f32) {
self.transform = self.transform.pre_translate(x, y);
}
fn render_rectangle(&mut self, top_left: Pos, bottom_right: Pos, gradient: &Gradient) {
if let Some(colors) = decode_gradient(gradient) {
self.backend_render_rectangle(top_left, bottom_right, colors);
}
}
fn render_top_rectangle(&mut self, top_left: Pos, bottom_right: Pos, gradient: &Gradient) {
if let Some(colors) = decode_gradient(gradient) {
self.backend_render_top_rectangle(top_left, bottom_right, colors);
}
}
fn render_icon(
&mut self,
[mut x, mut y]: Pos,
[mut width, mut height]: Pos,
icon: &Icon<A::Image>,
) {
let box_aspect_ratio = width / height;
let aspect_ratio_diff = box_aspect_ratio / icon.aspect_ratio;
if aspect_ratio_diff > 1.0 {
let new_width = width / aspect_ratio_diff;
let diff_width = width - new_width;
x += 0.5 * diff_width;
width = new_width;
} else if aspect_ratio_diff < 1.0 {
let new_height = height * aspect_ratio_diff;
let diff_height = height - new_height;
y += 0.5 * diff_height;
height = new_height;
}
let transform = self.transform.pre_translate(x, y).pre_scale(width, height);
self.scene
.bottom_layer_mut()
.push(Entity::Image(icon.image.share(), transform));
}
fn render_key_value_component(
&mut self,
key: &str,
abbreviations: &[Cow<'_, str>],
key_label: &mut AbbreviatedLabel<A::Label>,
value: &str,
value_label: &mut CachedLabel<A::Label>,
updates_frequently: bool,
[width, height]: [f32; 2],
key_color: Color,
value_color: Color,
display_two_rows: bool,
) {
let left_of_value_x = self.render_numbers(
value,
value_label,
Layer::from_updates_frequently(updates_frequently),
[width - PADDING, height + TEXT_ALIGN_BOTTOM],
DEFAULT_TEXT_SIZE,
solid(&value_color),
);
let end_x = if display_two_rows {
width
} else {
left_of_value_x
};
self.render_abbreviated_text_ellipsis(
iter::once(key).chain(abbreviations.iter().map(|x| &**x)),
key_label,
[PADDING, TEXT_ALIGN_TOP],
DEFAULT_TEXT_SIZE,
solid(&key_color),
end_x - PADDING,
);
}
fn render_abbreviated_text_ellipsis<'a>(
&mut self,
abbreviations: impl IntoIterator<Item = &'a str> + Clone,
label: &mut AbbreviatedLabel<A::Label>,
pos @ [x, _]: Pos,
scale: f32,
shader: FillShader,
max_x: f32,
) -> f32 {
let label = label.update(
abbreviations,
&mut self.handles,
&mut self.fonts.text.font,
(max_x - x) / scale,
);
self.scene.bottom_layer_mut().push(Entity::Label(
label.share(),
shader,
font::left_aligned(&self.transform, pos, scale),
));
x + label.width(scale)
}
fn render_text_ellipsis(
&mut self,
text: &str,
label: &mut CachedLabel<A::Label>,
pos @ [x, _]: Pos,
scale: f32,
shader: FillShader,
max_x: f32,
) -> f32 {
let label = label.update(
text,
&mut self.handles,
&mut self.fonts.text.font,
Some((max_x - x) / scale),
);
self.scene.bottom_layer_mut().push(Entity::Label(
label.share(),
shader,
font::left_aligned(&self.transform, pos, scale),
));
x + label.width(scale)
}
fn render_text_centered(
&mut self,
text: &str,
label: &mut CachedLabel<A::Label>,
min_x: f32,
max_x: f32,
pos: Pos,
scale: f32,
shader: FillShader,
) {
let label = label.update(
text,
&mut self.handles,
&mut self.fonts.text.font,
Some((max_x - min_x) / scale),
);
self.scene.bottom_layer_mut().push(Entity::Label(
label.share(),
shader,
font::centered(
&self.transform,
pos,
scale,
label.width(scale),
min_x,
max_x,
),
));
}
fn render_abbreviated_text_centered<'a>(
&mut self,
abbreviations: impl IntoIterator<Item = &'a str> + Clone,
label: &mut AbbreviatedLabel<A::Label>,
min_x: f32,
max_x: f32,
pos: Pos,
scale: f32,
shader: FillShader,
) {
let label = label.update(
abbreviations,
&mut self.handles,
&mut self.fonts.text.font,
(max_x - min_x) / scale,
);
self.scene.bottom_layer_mut().push(Entity::Label(
label.share(),
shader,
font::centered(
&self.transform,
pos,
scale,
label.width(scale),
min_x,
max_x,
),
));
}
fn render_text_right_align(
&mut self,
text: &str,
label: &mut CachedLabel<A::Label>,
layer: Layer,
pos @ [x, _]: Pos,
scale: f32,
shader: FillShader,
) -> f32 {
let label = label.update(text, &mut self.handles, &mut self.fonts.text.font, None);
let width = label.width(scale);
self.scene.layer_mut(layer).push(Entity::Label(
label.share(),
shader,
font::right_aligned(&self.transform, pos, scale, width),
));
x - width
}
fn render_abbreviated_text_align<'a>(
&mut self,
abbreviations: impl IntoIterator<Item = &'a str> + Clone,
label: &mut AbbreviatedLabel<A::Label>,
min_x: f32,
max_x: f32,
pos: Pos,
scale: f32,
centered: bool,
shader: FillShader,
) {
if centered {
self.render_abbreviated_text_centered(
abbreviations,
label,
min_x,
max_x,
pos,
scale,
shader,
);
} else {
self.render_abbreviated_text_ellipsis(abbreviations, label, pos, scale, shader, max_x);
}
}
fn render_numbers(
&mut self,
text: &str,
label: &mut CachedLabel<A::Label>,
layer: Layer,
pos @ [x, _]: Pos,
scale: f32,
shader: FillShader,
) -> f32 {
let label = label.update(text, &mut self.handles, &mut self.fonts.times.font, None);
let width = label.width(scale);
self.scene.layer_mut(layer).push(Entity::Label(
label.share(),
shader,
font::right_aligned(&self.transform, pos, scale, width),
));
x - width
}
fn render_timer(
&mut self,
text: &str,
label: &mut CachedLabel<A::Label>,
layer: Layer,
pos @ [x, _]: Pos,
scale: f32,
shader: FillShader,
) -> f32 {
let label = label.update(text, &mut self.handles, &mut self.fonts.timer.font, None);
let width = label.width(scale);
self.scene.layer_mut(layer).push(Entity::Label(
label.share(),
shader,
font::right_aligned(&self.transform, pos, scale, width),
));
x - width
}
fn measure_numbers(
&mut self,
text: &str,
label: &mut CachedLabel<A::Label>,
scale: f32,
) -> f32 {
let label = label.update(text, &mut self.handles, &mut self.fonts.times.font, None);
label.width(scale)
}
}
const fn decode_gradient(gradient: &Gradient) -> Option<FillShader> {
Some(match gradient {
Gradient::Transparent => return None,
Gradient::Horizontal(left, right) => {
FillShader::HorizontalGradient(left.to_array(), right.to_array())
}
Gradient::Vertical(top, bottom) => {
FillShader::VerticalGradient(top.to_array(), bottom.to_array())
}
Gradient::Plain(plain) => FillShader::SolidColor(plain.to_array()),
})
}
const fn solid(color: &Color) -> FillShader {
FillShader::SolidColor(color.to_array())
}
impl Transform {
const fn scale(scale_x: f32, scale_y: f32) -> Transform {
Self {
x: 0.0,
y: 0.0,
scale_x,
scale_y,
}
}
pub fn pre_scale(&self, scale_x: f32, scale_y: f32) -> Transform {
Self {
scale_x: self.scale_x * scale_x,
scale_y: self.scale_y * scale_y,
x: self.x,
y: self.y,
}
}
pub fn pre_translate(&self, x: f32, y: f32) -> Transform {
Self {
scale_x: self.scale_x,
scale_y: self.scale_y,
x: self.x + self.scale_x * x,
y: self.y + self.scale_y * y,
}
}
#[cfg(feature = "software-rendering")]
fn transform_y(&self, y: f32) -> f32 {
self.y + self.scale_y * y
}
}