slint/internal/backends/mcu/renderer.rs
Simon Hausmann f912ec7e6b Fix scaling of glyphs and improve type safety in the MCU backend
The code was mixing logical and physical sizes, causing glyphs being
doubly scaled down. Instead, this patch introduces:

 * Physical* and Logical* euclid length/size/rect aliases
 * some extraction traits for getting the scalars in rects/sizes as lengths (until euclid has them
built-in)
 * wrapper traits/types for safely extracting the physical font metrics the
 compiler generates (i16)
 * Fix a bug in the text height calculation where we failed to take the
   descent into account
2022-02-17 15:07:57 +01:00

644 lines
24 KiB
Rust

// Copyright © SixtyFPS GmbH <info@slint-ui.com>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-commercial
use alloc::collections::VecDeque;
use alloc::rc::Rc;
use alloc::{vec, vec::Vec};
use core::pin::Pin;
use embedded_graphics::pixelcolor::Rgb888;
use embedded_graphics::prelude::*;
use i_slint_core::graphics::{FontRequest, IntRect, PixelFormat, Rect as RectF};
use i_slint_core::{Color, ImageInner};
use crate::fonts::FontMetrics;
use crate::{
Devices, LogicalItemGeometry, LogicalPoint, LogicalRect, PhysicalLength, PhysicalPoint,
PhysicalRect, PhysicalSize, PointLengths, RectLengths, ScaleFactor, SizeLengths,
};
use euclid::num::Zero;
pub fn render_window_frame(
runtime_window: Rc<i_slint_core::window::Window>,
background: Rgb888,
devices: &mut dyn Devices,
) {
let size = devices.screen_size();
let mut scene = prepare_scene(runtime_window, size);
/*for item in scene.future_items {
match item.command {
SceneCommand::Rectangle { color } => {
embedded_graphics::primitives::Rectangle {
top_left: Point { x: item.x as _, y: item.y as _ },
size: Size { width: item.width as _, height: item.height as _ },
}
.into_styled(
embedded_graphics::primitives::PrimitiveStyleBuilder::new()
.fill_color(Rgb888::new(color.red(), color.green(), color.blue()))
.build(),
)
.draw(display)
.unwrap();
}
SceneCommand::Texture { data, format, stride, source_width, source_height, color } => {
let sx = item.width as f32 / source_width as f32;
let sy = item.height as f32 / source_height as f32;
let bpp = bpp(format) as usize;
for y in 0..item.height {
let pixel_iter = (0..item.width).into_iter().map(|x| {
let pos = ((y as f32 / sy) as usize * stride as usize)
+ (x as f32 / sx) as usize * bpp;
to_color(&data[pos..], format, color)
});
display
.fill_contiguous(
&embedded_graphics::primitives::Rectangle::new(
Point::new(item.x as i32, (item.y + y) as i32),
Size::new(item.width as u32, 1),
),
pixel_iter,
)
.unwrap()
}
}
}
}*/
let mut line_buffer = vec![background; size.width as usize];
while scene.current_line < size.height_length() {
line_buffer.fill(background);
let line = scene.process_line();
for span in line.spans.iter().rev() {
match span.command {
SceneCommand::Rectangle => {
let color = scene.rectangles[span.data_index];
let alpha = color.alpha();
if alpha == u8::MAX {
line_buffer[span.x.get() as usize..(span.x + span.width).get() as usize]
.fill(to_rgb888_color_discard_alpha(color))
} else {
for pix in &mut line_buffer
[span.x.get() as usize..(span.x + span.width).get() as usize]
{
let a = (u8::MAX - alpha) as u16;
let b = alpha as u16;
*pix = Rgb888::new(
((pix.r() as u16 * a + color.red() as u16 * b) >> 8) as u8,
((pix.g() as u16 * a + color.green() as u16 * b) >> 8) as u8,
((pix.b() as u16 * a + color.blue() as u16 * b) >> 8) as u8,
);
}
}
}
SceneCommand::Texture => {
let SceneTexture { data, format, stride, source_width, source_height, color } =
scene.textures[span.data_index];
let sx = span.width.get() as f32 / source_width.get() as f32;
let sy = span.height.get() as f32 / source_height.get() as f32;
let bpp = bpp(format) as usize;
let y = line.line - span.y;
for (x, pix) in line_buffer
[(span.x).get() as usize..(span.x + span.width).get() as usize]
.iter_mut()
.enumerate()
{
let pos = ((y.get() as f32 / sy) as usize * stride as usize)
+ (x as f32 / sx) as usize * bpp;
*pix = match format {
PixelFormat::Rgb => {
Rgb888::new(data[pos + 0], data[pos + 1], data[pos + 2])
}
PixelFormat::Rgba => {
if color.alpha() == 0 {
let a = (u8::MAX - data[pos + 3]) as u16;
let b = data[pos + 3] as u16;
Rgb888::new(
((pix.r() as u16 * a + data[pos + 0] as u16 * b) >> 8)
as u8,
((pix.g() as u16 * a + data[pos + 1] as u16 * b) >> 8)
as u8,
((pix.b() as u16 * a + data[pos + 2] as u16 * b) >> 8)
as u8,
)
} else {
let a = (u8::MAX - data[pos + 3]) as u16;
let b = data[pos + 3] as u16;
Rgb888::new(
((pix.r() as u16 * a + color.red() as u16 * b) >> 8) as u8,
((pix.g() as u16 * a + color.green() as u16 * b) >> 8)
as u8,
((pix.b() as u16 * a + color.blue() as u16 * b) >> 8) as u8,
)
}
}
PixelFormat::AlphaMap => {
let a = (u8::MAX - data[pos]) as u16;
let b = data[pos] as u16;
Rgb888::new(
((pix.r() as u16 * a + color.red() as u16 * b) >> 8) as u8,
((pix.g() as u16 * a + color.green() as u16 * b) >> 8) as u8,
((pix.b() as u16 * a + color.blue() as u16 * b) >> 8) as u8,
)
}
}
}
}
}
}
devices.fill_region(euclid::rect(0, line.line.get() as i16, size.width, 1), &line_buffer)
}
}
struct Scene {
/// the next line to be processed
current_line: PhysicalLength,
/// Element that have `y > current_line`
/// They must be sorted by `y` in reverse order (bottom to top)
/// then by `z` top to bottom
future_items: Vec<SceneItem>,
/// The items that overlap with the current line, sorted by z top to bottom
current_items: VecDeque<SceneItem>,
/// Some staging buffer of scene item
next_items: VecDeque<SceneItem>,
rectangles: Vec<Color>,
textures: Vec<SceneTexture>,
}
impl Scene {
fn new(mut items: Vec<SceneItem>, rectangles: Vec<Color>, textures: Vec<SceneTexture>) -> Self {
items.sort_unstable_by(|a, b| compare_scene_item(a, b).reverse());
Self {
future_items: items,
current_line: PhysicalLength::zero(),
current_items: Default::default(),
next_items: Default::default(),
rectangles,
textures,
}
}
/// Will generate a LineCommand for the current_line, remove all items that are done from the items
fn process_line(&mut self) -> LineCommand {
let mut command = vec![];
// Take the next element from current_items or future_items
loop {
let a_next_z =
self.future_items.last().filter(|i| i.y == self.current_line).map(|i| i.z);
let b_next_z = self.current_items.front().map(|i| i.z);
let item = match (a_next_z, b_next_z) {
(Some(a), Some(b)) => {
if a > b {
self.future_items.pop()
} else {
self.current_items.pop_front()
}
}
(Some(_), None) => self.future_items.pop(),
(None, Some(_)) => self.current_items.pop_front(),
_ => break,
};
let item = item.unwrap();
if item.y + item.height > self.current_line + PhysicalLength::new(1) {
self.next_items.push_back(item.clone());
}
command.push(item);
}
core::mem::swap(&mut self.next_items, &mut self.current_items);
let line = self.current_line;
self.current_line += PhysicalLength::new(1);
LineCommand { spans: command, line }
}
}
#[derive(Clone, Copy)]
struct SceneItem {
x: PhysicalLength,
y: PhysicalLength,
width: PhysicalLength,
height: PhysicalLength,
// this is the order of the item from which it is in the item tree
z: u16,
command: SceneCommand,
data_index: usize, // SceneCommand specific index in textures, etc. arrays for additional data
}
struct LineCommand {
line: PhysicalLength,
// Fixme: we need to process these so we do not draw items under opaque regions
spans: Vec<SceneItem>,
}
fn compare_scene_item(a: &SceneItem, b: &SceneItem) -> core::cmp::Ordering {
// First, order by line (top to bottom)
match a.y.partial_cmp(&b.y) {
None | Some(core::cmp::Ordering::Equal) => {}
Some(ord) => return ord,
}
// Then by the reverse z (front to back)
match a.z.partial_cmp(&b.z) {
None | Some(core::cmp::Ordering::Equal) => {}
Some(ord) => return ord.reverse(),
}
// anything else, we don't care
core::cmp::Ordering::Equal
}
#[derive(Clone, Copy)]
#[repr(u8)]
enum SceneCommand {
Rectangle,
Texture,
}
struct SceneTexture {
data: &'static [u8],
format: PixelFormat,
/// bytes between two lines in the source
stride: u16,
source_width: PhysicalLength,
source_height: PhysicalLength,
color: Color,
}
fn prepare_scene(runtime_window: Rc<i_slint_core::window::Window>, size: PhysicalSize) -> Scene {
let mut prepare_scene = PrepareScene::new(
size,
ScaleFactor::new(runtime_window.scale_factor()),
runtime_window.default_font_properties(),
);
runtime_window.draw_contents(|components| {
for (component, origin) in components {
i_slint_core::item_rendering::render_component_items(
component,
&mut prepare_scene,
origin.clone(),
);
}
});
Scene::new(prepare_scene.items, prepare_scene.rectangles, prepare_scene.textures)
}
struct PrepareScene {
items: Vec<SceneItem>,
rectangles: Vec<Color>,
textures: Vec<SceneTexture>,
state_stack: Vec<RenderState>,
current_state: RenderState,
scale_factor: ScaleFactor,
default_font: FontRequest,
}
impl PrepareScene {
fn new(size: PhysicalSize, scale_factor: ScaleFactor, default_font: FontRequest) -> Self {
Self {
items: vec![],
rectangles: vec![],
textures: vec![],
state_stack: vec![],
current_state: RenderState {
alpha: 1.,
offset: LogicalPoint::default(),
clip: LogicalRect::new(
LogicalPoint::default(),
size.cast() * scale_factor.inverse(),
),
},
scale_factor,
default_font,
}
}
fn should_draw(&self, rect: &LogicalRect) -> bool {
!rect.size.is_empty()
&& self.current_state.alpha > 0.01
&& self.current_state.clip.intersects(rect)
}
fn new_scene_rectangle(&mut self, geometry: LogicalRect, color: Color) {
self.rectangles.push(color);
self.new_scene_item(geometry, SceneCommand::Rectangle, self.rectangles.len() - 1);
}
fn new_scene_texture(&mut self, geometry: LogicalRect, texture: SceneTexture) {
self.textures.push(texture);
self.new_scene_item(geometry, SceneCommand::Texture, self.textures.len() - 1);
}
fn new_scene_item(&mut self, geometry: LogicalRect, command: SceneCommand, data_index: usize) {
let z = self.items.len() as u16;
self.items.push(SceneItem {
x: ((self.current_state.offset.x_length() + geometry.origin.x_length())
* self.scale_factor)
.cast(),
y: ((self.current_state.offset.y_length() + geometry.origin.y_length())
* self.scale_factor)
.cast(),
width: (geometry.size.width_length() * self.scale_factor).cast(),
height: (geometry.size.height_length() * self.scale_factor).cast(),
z,
command,
data_index,
});
}
fn draw_image_impl(
&mut self,
geom: LogicalRect,
source: &i_slint_core::graphics::Image,
source_clip: IntRect,
colorize: Color,
) {
let image_inner: &ImageInner = source.into();
match image_inner {
ImageInner::None => (),
ImageInner::AbsoluteFilePath(_) | ImageInner::EmbeddedData { .. } => {
unimplemented!()
}
ImageInner::EmbeddedImage(_) => todo!(),
ImageInner::StaticTextures { size, data, textures } => {
let sx = geom.width() / (size.width as f32);
let sy = geom.height() / (size.height as f32);
for t in textures.as_slice() {
if let Some(dest_rect) = t.rect.intersection(&source_clip).and_then(|r| {
r.intersection(
&self
.current_state
.clip
.to_untyped()
.scale(1. / sx, 1. / sy)
.round_in()
.cast(),
)
}) {
let actual_x = dest_rect.origin.x - t.rect.origin.x;
let actual_y = dest_rect.origin.y - t.rect.origin.y;
let stride = t.rect.width() as u16 * bpp(t.format);
self.new_scene_texture(
LogicalRect::from_untyped(&dest_rect.cast().scale(sx, sy)),
SceneTexture {
data: &data.as_slice()[(t.index
+ (stride as usize) * (actual_y as usize)
+ (bpp(t.format) as usize) * (actual_x as usize))..],
stride,
source_height: PhysicalLength::new(dest_rect.height() as i16),
source_width: PhysicalLength::new(dest_rect.width() as i16),
format: t.format,
color: if colorize.alpha() > 0 { colorize } else { t.color },
},
);
}
}
}
};
}
}
#[derive(Clone, Copy)]
struct RenderState {
alpha: f32,
offset: LogicalPoint,
clip: LogicalRect,
}
impl i_slint_core::item_rendering::ItemRenderer for PrepareScene {
fn draw_rectangle(&mut self, rect: Pin<&i_slint_core::items::Rectangle>) {
let geom = LogicalRect::new(LogicalPoint::default(), rect.logical_geometry().size_length());
if self.should_draw(&geom) {
let geom = match geom.intersection(&self.current_state.clip) {
Some(geom) => geom,
None => return,
};
// FIXME: gradients
let color = rect.background().color();
if color.alpha() == 0 {
return;
}
self.new_scene_rectangle(geom, color);
}
}
fn draw_border_rectangle(&mut self, rect: Pin<&i_slint_core::items::BorderRectangle>) {
let geom = LogicalRect::new(LogicalPoint::default(), rect.logical_geometry().size_length());
if self.should_draw(&geom) {
let border = rect.border_width();
// FIXME: gradients
let color = rect.background().color();
if color.alpha() > 0 {
if let Some(r) =
geom.inflate(-border, -border).intersection(&self.current_state.clip)
{
self.new_scene_rectangle(r, color);
}
}
if border > 0.01 {
// FIXME: radius
// FIXME: gradients
let border_color = rect.border_color().color();
if border_color.alpha() > 0 {
let mut add_border = |r: LogicalRect| {
if let Some(r) = r.intersection(&self.current_state.clip) {
self.new_scene_rectangle(r, border_color);
}
};
add_border(euclid::rect(0., 0., geom.width(), border));
add_border(euclid::rect(0., geom.height() - border, geom.width(), border));
add_border(euclid::rect(0., border, border, geom.height() - border - border));
add_border(euclid::rect(
geom.width() - border,
border,
border,
geom.height() - border - border,
));
}
}
}
}
fn draw_image(&mut self, image: Pin<&i_slint_core::items::ImageItem>) {
let geom =
LogicalRect::new(LogicalPoint::default(), image.logical_geometry().size_length());
if self.should_draw(&geom) {
self.draw_image_impl(
geom,
&image.source(),
euclid::rect(0, 0, i32::MAX, i32::MAX),
Default::default(),
);
}
}
fn draw_clipped_image(&mut self, image: Pin<&i_slint_core::items::ClippedImage>) {
// when the source_clip size is empty, make it full
let a = |v| if v == 0 { i32::MAX } else { v };
let geom =
LogicalRect::new(LogicalPoint::default(), image.logical_geometry().size_length());
if self.should_draw(&geom) {
self.draw_image_impl(
geom,
&image.source(),
euclid::rect(
image.source_clip_x(),
image.source_clip_y(),
a(image.source_clip_width()),
a(image.source_clip_height()),
),
image.colorize().color(),
);
}
}
fn draw_text(&mut self, text: Pin<&i_slint_core::items::Text>) {
let font_request = text.unresolved_font_request().merge(&self.default_font);
let (font, glyphs) = crate::fonts::match_font(&font_request, self.scale_factor);
let color = text.color().color();
let baseline_y = glyphs.ascent(font);
for (glyph_baseline_x, glyph) in crate::fonts::glyphs_for_text(font, glyphs, &text.text()) {
if let Some(dest_rect) = (PhysicalRect::new(
PhysicalPoint::from_lengths(
glyph_baseline_x + glyph.x(),
baseline_y - glyph.y() - glyph.height(),
),
glyph.size(),
)
.cast()
* self.scale_factor.inverse())
.intersection(&self.current_state.clip)
{
let stride = glyph.width().get() as u16;
self.new_scene_texture(
dest_rect,
SceneTexture {
data: glyph.data().as_slice(),
stride,
source_width: glyph.width(),
source_height: glyph.height(),
format: PixelFormat::AlphaMap,
color,
},
);
}
}
}
fn draw_text_input(&mut self, _text_input: Pin<&i_slint_core::items::TextInput>) {
// TODO
}
#[cfg(feature = "simulator")]
fn draw_path(&mut self, _path: Pin<&i_slint_core::items::Path>) {
// TODO
}
fn draw_box_shadow(&mut self, _box_shadow: Pin<&i_slint_core::items::BoxShadow>) {
// TODO
}
fn combine_clip(&mut self, other: RectF, _radius: f32, _border_width: f32) {
match self.current_state.clip.intersection(&LogicalRect::from_untyped(&other)) {
Some(r) => {
self.current_state.clip = r;
}
None => {
self.current_state.clip = LogicalRect::default();
}
};
// TODO: handle radius and border
}
fn get_current_clip(&self) -> i_slint_core::graphics::Rect {
self.current_state.clip.to_untyped()
}
fn translate(&mut self, x: f32, y: f32) {
self.current_state.offset.x += x;
self.current_state.offset.y += y;
self.current_state.clip = self.current_state.clip.translate((-x, -y).into())
}
fn rotate(&mut self, _angle_in_degrees: f32) {
todo!()
}
fn apply_opacity(&mut self, opacity: f32) {
self.current_state.alpha *= opacity;
}
fn save_state(&mut self) {
self.state_stack.push(self.current_state);
}
fn restore_state(&mut self) {
self.current_state = self.state_stack.pop().unwrap();
}
fn scale_factor(&self) -> f32 {
self.scale_factor.0
}
fn draw_cached_pixmap(
&mut self,
_item_cache: &i_slint_core::item_rendering::CachedRenderingData,
_update_fn: &dyn Fn(&mut dyn FnMut(u32, u32, &[u8])),
) {
todo!()
}
fn draw_string(&mut self, _string: &str, _color: Color) {
todo!()
}
fn window(&self) -> i_slint_core::window::WindowRc {
unreachable!("this backend don't query the window")
}
fn as_any(&mut self) -> &mut dyn core::any::Any {
self
}
}
/// bytes per pixels
fn bpp(format: PixelFormat) -> u16 {
match format {
PixelFormat::Rgb => 3,
PixelFormat::Rgba => 4,
PixelFormat::AlphaMap => 1,
}
}
/*
fn to_color(data: &[u8], format: PixelFormat, color: Color) -> Rgb888 {
match format {
PixelFormat::Rgba if color.alpha() > 0 => Rgb888::new(
((color.red() as u16 * data[3] as u16) >> 8) as u8,
((color.green() as u16 * data[3] as u16) >> 8) as u8,
((color.blue() as u16 * data[3] as u16) >> 8) as u8,
),
PixelFormat::Rgb => Rgb888::new(data[0], data[1], data[2]),
PixelFormat::Rgba => Rgb888::new(data[0], data[1], data[2]),
PixelFormat::AlphaMap => Rgb888::new(
((color.red() as u16 * data[0] as u16) >> 8) as u8,
((color.green() as u16 * data[0] as u16) >> 8) as u8,
((color.blue() as u16 * data[0] as u16) >> 8) as u8,
),
}
}*/
pub fn to_rgb888_color_discard_alpha(col: Color) -> Rgb888 {
Rgb888::new(col.red(), col.green(), col.blue())
}