mirror of
https://github.com/slint-ui/slint.git
synced 2025-08-31 15:47:26 +00:00

Note that this is still disabled in the compiler with no way to enable it with public API
432 lines
16 KiB
Rust
432 lines
16 KiB
Rust
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
|
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
|
|
|
//! This is the module contain data structures for a scene of items that can be rendered
|
|
|
|
use super::{
|
|
Fixed, PhysicalBorderRadius, PhysicalLength, PhysicalPoint, PhysicalRect, PhysicalRegion,
|
|
PhysicalSize, PremultipliedRgbaColor, RenderingRotation,
|
|
};
|
|
use crate::graphics::{PixelFormat, SharedImageBuffer};
|
|
use crate::lengths::{PointLengths as _, SizeLengths as _};
|
|
use crate::Color;
|
|
use alloc::rc::Rc;
|
|
#[cfg(not(feature = "std"))]
|
|
use alloc::vec::Vec;
|
|
use euclid::Length;
|
|
|
|
#[derive(Default)]
|
|
pub struct SceneVectors {
|
|
pub textures: Vec<SceneTexture<'static>>,
|
|
pub rounded_rectangles: Vec<RoundedRectangle>,
|
|
pub shared_buffers: Vec<SharedBufferCommand>,
|
|
pub gradients: Vec<GradientCommand>,
|
|
}
|
|
|
|
pub struct Scene {
|
|
/// the next line to be processed
|
|
pub(super) current_line: PhysicalLength,
|
|
|
|
/// The items are sorted like so:
|
|
/// - `items[future_items_index..]` are the items that have `y > current_line`.
|
|
/// They must be sorted by `y` (top to bottom), then by `z` (front to back)
|
|
/// - `items[..current_items_index]` are the items that overlap with the current_line,
|
|
/// sorted by z (front to back)
|
|
pub(super) items: Vec<SceneItem>,
|
|
|
|
pub(super) vectors: SceneVectors,
|
|
|
|
pub(super) future_items_index: usize,
|
|
pub(super) current_items_index: usize,
|
|
|
|
pub(super) dirty_region: PhysicalRegion,
|
|
|
|
pub(super) current_line_ranges: Vec<core::ops::Range<i16>>,
|
|
pub(super) range_valid_until_line: PhysicalLength,
|
|
}
|
|
|
|
impl Scene {
|
|
pub fn new(
|
|
mut items: Vec<SceneItem>,
|
|
vectors: SceneVectors,
|
|
dirty_region: PhysicalRegion,
|
|
) -> Self {
|
|
let current_line =
|
|
dirty_region.iter_box().map(|x| x.min.y_length()).min().unwrap_or_default();
|
|
items.retain(|i| i.pos.y_length() + i.size.height_length() > current_line);
|
|
items.sort_unstable_by(compare_scene_item);
|
|
let current_items_index = items.partition_point(|i| i.pos.y_length() <= current_line);
|
|
items[..current_items_index].sort_unstable_by(|a, b| b.z.cmp(&a.z));
|
|
let mut r = Self {
|
|
items,
|
|
current_line,
|
|
current_items_index,
|
|
future_items_index: current_items_index,
|
|
vectors,
|
|
dirty_region,
|
|
current_line_ranges: Default::default(),
|
|
range_valid_until_line: Default::default(),
|
|
};
|
|
r.recompute_ranges();
|
|
debug_assert_eq!(r.current_line, r.dirty_region.bounding_rect().origin.y_length());
|
|
r
|
|
}
|
|
|
|
/// Updates `current_items_index` and `future_items_index` to match the invariant
|
|
pub fn next_line(&mut self) {
|
|
self.current_line += PhysicalLength::new(1);
|
|
|
|
let skipped = self.current_line >= self.range_valid_until_line && self.recompute_ranges();
|
|
|
|
// The items array is split in part:
|
|
// 1. [0..i] are the items that have already been processed, that are on this line
|
|
// 2. [j..current_items_index] are the items from the previous line that might still be
|
|
// valid on this line
|
|
// 3. [tmp1, tmp2] is a buffer where we swap items so we can make room for the items in [0..i]
|
|
// 4. [future_items_index..] are the items which might get processed now
|
|
// 5. [current_items_index..tmp1], [tmp2..future_items_index] and [i..j] is garbage
|
|
//
|
|
// At each step, we selecting the item with the higher z from the list 2 or 3 or 4 and take it from
|
|
// that list. Then we add it to the list [0..i] if it needs more processing. If needed,
|
|
// we move the first item from list 2. to list 3. to make some room
|
|
|
|
let (mut i, mut j, mut tmp1, mut tmp2) =
|
|
(0, 0, self.current_items_index, self.current_items_index);
|
|
|
|
if skipped {
|
|
// Merge sort doesn't work in that case.
|
|
while j < self.current_items_index {
|
|
let item = self.items[j];
|
|
if item.pos.y_length() + item.size.height_length() > self.current_line {
|
|
self.items[i] = item;
|
|
i += 1;
|
|
}
|
|
j += 1;
|
|
}
|
|
while self.future_items_index < self.items.len() {
|
|
let item = self.items[self.future_items_index];
|
|
if item.pos.y_length() > self.current_line {
|
|
break;
|
|
}
|
|
self.future_items_index += 1;
|
|
if item.pos.y_length() + item.size.height_length() < self.current_line {
|
|
continue;
|
|
}
|
|
self.items[i] = item;
|
|
i += 1;
|
|
}
|
|
self.items[0..i].sort_unstable_by(|a, b| b.z.cmp(&a.z));
|
|
self.current_items_index = i;
|
|
return;
|
|
}
|
|
|
|
'outer: loop {
|
|
let future_next_z = self
|
|
.items
|
|
.get(self.future_items_index)
|
|
.filter(|i| i.pos.y_length() <= self.current_line)
|
|
.map(|i| i.z);
|
|
let item = loop {
|
|
if tmp1 != tmp2 {
|
|
if future_next_z.map_or(true, |z| self.items[tmp1].z > z) {
|
|
let idx = tmp1;
|
|
tmp1 += 1;
|
|
if tmp1 == tmp2 {
|
|
tmp1 = self.current_items_index;
|
|
tmp2 = self.current_items_index;
|
|
}
|
|
break self.items[idx];
|
|
}
|
|
} else if j < self.current_items_index {
|
|
let item = &self.items[j];
|
|
if item.pos.y_length() + item.size.height_length() <= self.current_line {
|
|
j += 1;
|
|
continue;
|
|
}
|
|
if future_next_z.map_or(true, |z| item.z > z) {
|
|
j += 1;
|
|
break *item;
|
|
}
|
|
}
|
|
if future_next_z.is_some() {
|
|
self.future_items_index += 1;
|
|
break self.items[self.future_items_index - 1];
|
|
}
|
|
break 'outer;
|
|
};
|
|
if i != j {
|
|
// there is room
|
|
} else if j >= self.current_items_index && tmp1 == tmp2 {
|
|
// the current_items list is empty
|
|
j += 1
|
|
} else if self.items[j].pos.y_length() + self.items[j].size.height_length()
|
|
<= self.current_line
|
|
{
|
|
// next item in the current_items array is no longer in this line
|
|
j += 1;
|
|
} else if tmp2 < self.future_items_index && j < self.current_items_index {
|
|
// move the next item in current_items
|
|
let to_move = self.items[j];
|
|
self.items[tmp2] = to_move;
|
|
j += 1;
|
|
tmp2 += 1;
|
|
} else {
|
|
debug_assert!(tmp1 >= self.current_items_index);
|
|
let sort_begin = i;
|
|
// merge sort doesn't work because we don't have enough tmp space, just bring all items and use a normal sort.
|
|
while j < self.current_items_index {
|
|
let item = self.items[j];
|
|
if item.pos.y_length() + item.size.height_length() > self.current_line {
|
|
self.items[i] = item;
|
|
i += 1;
|
|
}
|
|
j += 1;
|
|
}
|
|
self.items.copy_within(tmp1..tmp2, i);
|
|
i += tmp2 - tmp1;
|
|
debug_assert!(i < self.future_items_index);
|
|
self.items[i] = item;
|
|
i += 1;
|
|
while self.future_items_index < self.items.len() {
|
|
let item = self.items[self.future_items_index];
|
|
if item.pos.y_length() > self.current_line {
|
|
break;
|
|
}
|
|
self.future_items_index += 1;
|
|
self.items[i] = item;
|
|
i += 1;
|
|
}
|
|
self.items[sort_begin..i].sort_unstable_by(|a, b| b.z.cmp(&a.z));
|
|
break;
|
|
}
|
|
self.items[i] = item;
|
|
i += 1;
|
|
}
|
|
self.current_items_index = i;
|
|
// check that current items are properly sorted
|
|
debug_assert!(self.items[0..self.current_items_index].windows(2).all(|x| x[0].z >= x[1].z));
|
|
}
|
|
|
|
// return true if lines were skipped
|
|
fn recompute_ranges(&mut self) -> bool {
|
|
let validity = super::region_line_ranges(
|
|
&self.dirty_region,
|
|
self.current_line.get(),
|
|
&mut self.current_line_ranges,
|
|
);
|
|
if self.current_line_ranges.is_empty() {
|
|
if let Some(next) = validity {
|
|
self.current_line = Length::new(next);
|
|
self.range_valid_until_line = Length::new(
|
|
super::region_line_ranges(
|
|
&self.dirty_region,
|
|
self.current_line.get(),
|
|
&mut self.current_line_ranges,
|
|
)
|
|
.unwrap_or_default(),
|
|
);
|
|
return true;
|
|
}
|
|
}
|
|
self.range_valid_until_line = Length::new(validity.unwrap_or_default());
|
|
false
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub struct SceneItem {
|
|
pub pos: PhysicalPoint,
|
|
pub size: PhysicalSize,
|
|
// this is the order of the item from which it is in the item tree
|
|
pub z: u16,
|
|
pub command: SceneCommand,
|
|
}
|
|
|
|
fn compare_scene_item(a: &SceneItem, b: &SceneItem) -> core::cmp::Ordering {
|
|
// First, order by line (top to bottom)
|
|
match a.pos.y.partial_cmp(&b.pos.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, Debug)]
|
|
#[repr(u8)]
|
|
pub enum SceneCommand {
|
|
Rectangle {
|
|
color: PremultipliedRgbaColor,
|
|
},
|
|
/// texture_index is an index in the [`SceneVectors::textures`] array
|
|
Texture {
|
|
texture_index: u16,
|
|
},
|
|
/// shared_buffer_index is an index in [`SceneVectors::shared_buffers`]
|
|
SharedBuffer {
|
|
shared_buffer_index: u16,
|
|
},
|
|
/// rectangle_index is an index in the [`SceneVectors::rounded_rectangle`] array
|
|
RoundedRectangle {
|
|
rectangle_index: u16,
|
|
},
|
|
/// rectangle_index is an index in the [`SceneVectors::rounded_gradients`] array
|
|
Gradient {
|
|
gradient_index: u16,
|
|
},
|
|
}
|
|
|
|
pub struct SceneTexture<'a> {
|
|
/// This should have a size so that the entire slice is ((height - 1) * pixel_stride + width) * bpp
|
|
pub data: &'a [u8],
|
|
pub format: PixelFormat,
|
|
/// number of pixels between two lines in the source
|
|
pub pixel_stride: u16,
|
|
|
|
pub extra: SceneTextureExtra,
|
|
}
|
|
|
|
impl<'a> SceneTexture<'a> {
|
|
pub fn source_size(&self) -> PhysicalSize {
|
|
let mut len = self.data.len();
|
|
if self.format == PixelFormat::SignedDistanceField {
|
|
len -= 1;
|
|
} else {
|
|
len /= self.format.bpp();
|
|
}
|
|
let stride = self.pixel_stride as usize;
|
|
let h = len / stride;
|
|
let w = len % stride;
|
|
if w == 0 {
|
|
PhysicalSize::new(stride as _, h as _)
|
|
} else {
|
|
PhysicalSize::new(w as _, (h + 1) as _)
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub struct SceneTextureExtra {
|
|
/// Delta x: the amount of "image pixel" that we need to skip for each physical pixel in the target buffer
|
|
pub dx: Fixed<u16, 8>,
|
|
pub dy: Fixed<u16, 8>,
|
|
/// Offset which is the coordinate of the "image pixel" which going to be drawn at location SceneItem::pos
|
|
pub off_x: Fixed<u16, 4>,
|
|
pub off_y: Fixed<u16, 4>,
|
|
/// Color to colorize. When not transparent, consider that the image is an alpha map and always use that color.
|
|
/// The alpha of this color is ignored. (it is supposed to be mixed in `Self::alpha`)
|
|
pub colorize: Color,
|
|
pub alpha: u8,
|
|
pub rotation: RenderingRotation,
|
|
}
|
|
|
|
pub enum SharedBufferData {
|
|
SharedImage(SharedImageBuffer),
|
|
AlphaMap { data: Rc<[u8]>, width: u16 },
|
|
}
|
|
|
|
impl SharedBufferData {
|
|
fn width(&self) -> usize {
|
|
match self {
|
|
SharedBufferData::SharedImage(image) => image.width() as usize,
|
|
SharedBufferData::AlphaMap { width, .. } => *width as usize,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct SharedBufferCommand {
|
|
pub buffer: SharedBufferData,
|
|
/// The source rectangle that is mapped into this command span
|
|
pub source_rect: PhysicalRect,
|
|
pub extra: SceneTextureExtra,
|
|
}
|
|
|
|
impl SharedBufferCommand {
|
|
pub fn as_texture(&self) -> SceneTexture<'_> {
|
|
let stride = self.buffer.width();
|
|
let core::ops::Range { start, end } = compute_range_in_buffer(&self.source_rect, stride);
|
|
|
|
match &self.buffer {
|
|
SharedBufferData::SharedImage(SharedImageBuffer::RGB8(b)) => SceneTexture {
|
|
data: &b.as_bytes()[start * 3..end * 3],
|
|
pixel_stride: stride as u16,
|
|
format: PixelFormat::Rgb,
|
|
extra: self.extra,
|
|
},
|
|
SharedBufferData::SharedImage(SharedImageBuffer::RGBA8(b)) => SceneTexture {
|
|
data: &b.as_bytes()[start * 4..end * 4],
|
|
pixel_stride: stride as u16,
|
|
format: PixelFormat::Rgba,
|
|
extra: self.extra,
|
|
},
|
|
SharedBufferData::SharedImage(SharedImageBuffer::RGBA8Premultiplied(b)) => {
|
|
SceneTexture {
|
|
data: &b.as_bytes()[start * 4..end * 4],
|
|
pixel_stride: stride as u16,
|
|
format: PixelFormat::RgbaPremultiplied,
|
|
extra: self.extra,
|
|
}
|
|
}
|
|
SharedBufferData::AlphaMap { data, width } => SceneTexture {
|
|
data: &data[start..end],
|
|
pixel_stride: *width,
|
|
format: PixelFormat::AlphaMap,
|
|
extra: self.extra,
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Given a rectangle of coordinate in a buffer and a stride, compute the range, in pixel
|
|
pub fn compute_range_in_buffer(
|
|
source_rect: &PhysicalRect,
|
|
pixel_stride: usize,
|
|
) -> core::ops::Range<usize> {
|
|
let start = pixel_stride * source_rect.min_y() as usize + source_rect.min_x() as usize;
|
|
let end = pixel_stride * (source_rect.max_y() - 1) as usize + source_rect.max_x() as usize;
|
|
start..end
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct RoundedRectangle {
|
|
pub radius: PhysicalBorderRadius,
|
|
/// the border's width
|
|
pub width: PhysicalLength,
|
|
pub border_color: PremultipliedRgbaColor,
|
|
pub inner_color: PremultipliedRgbaColor,
|
|
/// The clips is the amount of pixels of the rounded rectangle that is clipped away.
|
|
/// For example, if left_clip > width, then the left border will not be visible, and
|
|
/// if left_clip > radius, then no radius will be seen in the left side
|
|
pub left_clip: PhysicalLength,
|
|
pub right_clip: PhysicalLength,
|
|
pub top_clip: PhysicalLength,
|
|
pub bottom_clip: PhysicalLength,
|
|
}
|
|
|
|
/// Goes from color 1 to color2
|
|
///
|
|
/// depending of `flags & 0b1`
|
|
/// - if false: on the left side, goes from `start` to 1, on the right side, goes from 0 to `1-start`
|
|
/// - if true: on the left side, goes from 0 to `1-start`, on the right side, goes from `start` to `1`
|
|
#[derive(Debug)]
|
|
pub struct GradientCommand {
|
|
pub color1: PremultipliedRgbaColor,
|
|
pub color2: PremultipliedRgbaColor,
|
|
pub start: u8,
|
|
/// bit 0: if the slope is positive or negative
|
|
/// bit 1: if we should fill with color1 on the left side when left_clip is negative (or transparent)
|
|
/// bit 2: if we should fill with color2 on the left side when right_clip is negative (or transparent)
|
|
pub flags: u8,
|
|
/// If positive, the clip has the same meaning as in RoundedRectangle.
|
|
/// If negative, that means the "stop" is only starting or stopping at that point
|
|
pub left_clip: PhysicalLength,
|
|
pub right_clip: PhysicalLength,
|
|
pub top_clip: PhysicalLength,
|
|
pub bottom_clip: PhysicalLength,
|
|
}
|