// Copyright © SixtyFPS GmbH // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 //! This module contains the [`SoftwareRenderer`] and related types. //! //! It is only enabled when the `renderer-software` Slint feature is enabled. #![warn(missing_docs)] mod draw_functions; mod fixed; mod fonts; mod minimal_software_window; mod scene; use self::fonts::GlyphRenderer; pub use self::minimal_software_window::MinimalSoftwareWindow; use self::scene::*; use crate::api::PlatformError; use crate::graphics::rendering_metrics_collector::{RefreshMode, RenderingMetricsCollector}; use crate::graphics::{BorderRadius, Rgba8Pixel, SharedImageBuffer, SharedPixelBuffer}; use crate::item_rendering::{ CachedRenderingData, DirtyRegion, PartialRenderingState, RenderBorderRectangle, RenderImage, RenderRectangle, }; use crate::items::{ItemRc, TextOverflow, TextWrap}; use crate::lengths::{ LogicalBorderRadius, LogicalLength, LogicalPoint, LogicalRect, LogicalSize, LogicalVector, PhysicalPx, PointLengths, RectLengths, ScaleFactor, SizeLengths, }; use crate::renderer::RendererSealed; use crate::textlayout::{AbstractFont, FontMetrics, TextParagraphLayout}; use crate::window::{WindowAdapter, WindowInner}; use crate::{Brush, Color, ImageInner, StaticTextures}; use alloc::rc::{Rc, Weak}; use alloc::{vec, vec::Vec}; use core::cell::{Cell, RefCell}; use core::pin::Pin; use euclid::Length; use fixed::Fixed; #[allow(unused)] use num_traits::Float; use num_traits::NumCast; pub use draw_functions::{PremultipliedRgbaColor, Rgb565Pixel, TargetPixel}; type PhysicalLength = euclid::Length; type PhysicalRect = euclid::Rect; type PhysicalSize = euclid::Size2D; type PhysicalPoint = euclid::Point2D; type PhysicalBorderRadius = BorderRadius; pub use crate::item_rendering::RepaintBufferType; /// This enum describes the rotation that should be applied to the contents rendered by the software renderer. /// /// Argument to be passed in [`SoftwareRenderer::set_rendering_rotation`]. #[non_exhaustive] #[derive(Default, Copy, Clone, Eq, PartialEq, Debug)] pub enum RenderingRotation { /// No rotation #[default] NoRotation, /// Rotate 90° to the right Rotate90, /// 180° rotation (upside-down) Rotate180, /// Rotate 90° to the left Rotate270, } impl RenderingRotation { fn is_transpose(self) -> bool { matches!(self, Self::Rotate90 | Self::Rotate270) } fn mirror_width(self) -> bool { matches!(self, Self::Rotate270 | Self::Rotate180) } fn mirror_height(self) -> bool { matches!(self, Self::Rotate90 | Self::Rotate180) } /// Angle of the rotation in degrees pub fn angle(self) -> f32 { match self { RenderingRotation::NoRotation => 0., RenderingRotation::Rotate90 => 90., RenderingRotation::Rotate180 => 180., RenderingRotation::Rotate270 => 270., } } } #[derive(Copy, Clone, Debug)] struct RotationInfo { orientation: RenderingRotation, screen_size: PhysicalSize, } /// Extension trait for euclid type to transpose coordinates (swap x and y, as well as width and height) trait Transform { /// Return a copy of Self whose coordinate are swapped (x swapped with y) #[must_use] fn transformed(self, info: RotationInfo) -> Self; } impl> Transform for euclid::Point2D { fn transformed(mut self, info: RotationInfo) -> Self { if info.orientation.mirror_width() { self.x = T::from(info.screen_size.width).unwrap() - self.x - T::from(1).unwrap() } if info.orientation.mirror_height() { self.y = T::from(info.screen_size.height).unwrap() - self.y - T::from(1).unwrap() } if info.orientation.is_transpose() { core::mem::swap(&mut self.x, &mut self.y); } self } } impl Transform for euclid::Size2D { fn transformed(mut self, info: RotationInfo) -> Self { if info.orientation.is_transpose() { core::mem::swap(&mut self.width, &mut self.height); } self } } impl> Transform for euclid::Rect { fn transformed(self, info: RotationInfo) -> Self { let one = T::from(1).unwrap(); let mut origin = self.origin.transformed(info); let size = self.size.transformed(info); if info.orientation.mirror_width() { origin.y = origin.y - (size.height - one); } if info.orientation.mirror_height() { origin.x = origin.x - (size.width - one); } Self::new(origin, size) } } impl Transform for BorderRadius { fn transformed(self, info: RotationInfo) -> Self { match info.orientation { RenderingRotation::NoRotation => self, RenderingRotation::Rotate90 => { Self::new(self.bottom_left, self.top_left, self.top_right, self.bottom_right) } RenderingRotation::Rotate180 => { Self::new(self.bottom_right, self.bottom_left, self.top_left, self.top_right) } RenderingRotation::Rotate270 => { Self::new(self.top_right, self.bottom_right, self.bottom_left, self.top_left) } } } } /// This trait defines a bi-directional interface between Slint and your code to send lines to your screen, when using /// the [`SoftwareRenderer::render_by_line`] function. /// /// * Through the associated `TargetPixel` type Slint knows how to create and manipulate pixels without having to know /// the exact device-specific binary representation and operations for blending. /// * Through the `process_line` function Slint notifies you when a line can be rendered and provides a callback that /// you can invoke to fill a slice of pixels for the given line. /// /// See the [`render_by_line`](SoftwareRenderer::render_by_line) documentation for an example. pub trait LineBufferProvider { /// The pixel type of the buffer type TargetPixel: TargetPixel; /// Called once per line, you will have to call the render_fn back with the buffer. /// /// The `line` is the y position of the line to be drawn. /// The `range` is the range within the line that is going to be rendered (eg, within the dirty region) /// The `render_fn` function should be called to render the line, passing the buffer /// corresponding to the specified line and range. fn process_line( &mut self, line: usize, range: core::ops::Range, render_fn: impl FnOnce(&mut [Self::TargetPixel]), ); } #[cfg(not(cbindgen))] const PHYSICAL_REGION_MAX_SIZE: usize = DirtyRegion::MAX_COUNT; // cbindgen can't understand associated const correctly, so hardcode the value #[cfg(cbindgen)] pub const PHYSICAL_REGION_MAX_SIZE: usize = 3; const _: () = { assert!(PHYSICAL_REGION_MAX_SIZE == 3); assert!(DirtyRegion::MAX_COUNT == 3); }; /// Represents a rectangular region on the screen, used for partial rendering. /// /// The region may be composed of multiple sub-regions. #[derive(Clone, Debug, Default)] #[repr(C)] pub struct PhysicalRegion { rectangles: [euclid::Box2D; PHYSICAL_REGION_MAX_SIZE], count: usize, } impl PhysicalRegion { fn iter_box(&self) -> impl Iterator> + '_ { (0..self.count).map(|x| self.rectangles[x]) } fn bounding_rect(&self) -> PhysicalRect { if self.count == 0 { return Default::default(); } let mut r = self.rectangles[0]; for i in 1..self.count { r = r.union(&self.rectangles[i]); } r.to_rect() } /// Returns the size of the bounding box of this region. pub fn bounding_box_size(&self) -> crate::api::PhysicalSize { let bb = self.bounding_rect(); crate::api::PhysicalSize { width: bb.width() as _, height: bb.height() as _ } } /// Returns the origin of the bounding box of this region. pub fn bounding_box_origin(&self) -> crate::api::PhysicalPosition { let bb = self.bounding_rect(); crate::api::PhysicalPosition { x: bb.origin.x as _, y: bb.origin.y as _ } } /// Returns an iterator over the rectangles in this region. /// Each rectangle is represented by its position and its size. /// They do not overlap. pub fn iter( &self, ) -> impl Iterator + '_ { let mut line_ranges = Vec::>::new(); let mut begin_line = 0; let mut end_line = 0; core::iter::from_fn(move || loop { match line_ranges.pop() { Some(r) => { return Some(( crate::api::PhysicalPosition { x: r.start as _, y: begin_line as _ }, crate::api::PhysicalSize { width: r.len() as _, height: (end_line - begin_line) as _, }, )); } None => { begin_line = end_line; end_line = match region_line_ranges(self, begin_line, &mut line_ranges) { Some(end_line) => end_line, None => return None, }; line_ranges.reverse(); } } }) } fn intersection(&self, clip: &PhysicalRect) -> PhysicalRegion { let mut res = Self::default(); let clip = clip.to_box2d(); let mut count = 0; for i in 0..self.count { if let Some(r) = self.rectangles[i].intersection(&clip) { res.rectangles[count] = r; count += 1; } } res.count = count; res } } #[test] fn region_iter() { let mut region = PhysicalRegion::default(); assert_eq!(region.iter().next(), None); region.rectangles[0] = euclid::Box2D::from_origin_and_size(euclid::point2(1, 1), euclid::size2(2, 3)); region.rectangles[1] = euclid::Box2D::from_origin_and_size(euclid::point2(6, 2), euclid::size2(3, 20)); region.rectangles[2] = euclid::Box2D::from_origin_and_size(euclid::point2(0, 10), euclid::size2(10, 5)); assert_eq!(region.iter().next(), None); region.count = 1; let r = |x, y, width, height| { (crate::api::PhysicalPosition { x, y }, crate::api::PhysicalSize { width, height }) }; let mut iter = region.iter(); assert_eq!(iter.next(), Some(r(1, 1, 2, 3))); assert_eq!(iter.next(), None); drop(iter); region.count = 3; let mut iter = region.iter(); assert_eq!(iter.next(), Some(r(1, 1, 2, 1))); // the two first rectangle could have been merged assert_eq!(iter.next(), Some(r(1, 2, 2, 2))); assert_eq!(iter.next(), Some(r(6, 2, 3, 2))); assert_eq!(iter.next(), Some(r(6, 4, 3, 6))); assert_eq!(iter.next(), Some(r(0, 10, 10, 5))); assert_eq!(iter.next(), Some(r(6, 15, 3, 7))); assert_eq!(iter.next(), None); } /// Computes what are the x ranges that intersects the region for specified y line. /// /// This uses a mutable reference to a Vec so that the memory is re-used between calls. /// /// Returns the y position until which this range is valid fn region_line_ranges( region: &PhysicalRegion, line: i16, line_ranges: &mut Vec>, ) -> Option { line_ranges.clear(); let mut next_validity = None::; for geom in region.iter_box() { if geom.is_empty() { continue; } if geom.y_range().contains(&line) { match &mut next_validity { Some(val) => *val = geom.max.y.min(*val), None => next_validity = Some(geom.max.y), } let mut tmp = Some(geom.x_range()); line_ranges.retain_mut(|it| { if let Some(r) = &mut tmp { if it.end < r.start { true } else if it.start <= r.start { if it.end >= r.end { tmp = None; return true; } r.start = it.start; return false; } else if it.start <= r.end { if it.end <= r.end { return false; } else { it.start = r.start; tmp = None; return true; } } else { core::mem::swap(it, r); return true; } } else { true } }); if let Some(r) = tmp { line_ranges.push(r); } continue; } else if geom.min.y >= line { match &mut next_validity { Some(val) => *val = geom.min.y.min(*val), None => next_validity = Some(geom.min.y), } } } // check that current items are properly sorted debug_assert!(line_ranges.windows(2).all(|x| x[0].end < x[1].start)); next_validity } mod target_pixel_buffer; #[cfg(feature = "experimental")] pub use target_pixel_buffer::{ DrawRectangleArgs, DrawTextureArgs, TargetPixelBuffer, TexturePixelFormat, }; #[cfg(not(feature = "experimental"))] use target_pixel_buffer::TexturePixelFormat; struct TargetPixelSlice<'a, T> { data: &'a mut [T], pixel_stride: usize, } impl<'a, T: TargetPixel> target_pixel_buffer::TargetPixelBuffer for TargetPixelSlice<'a, T> { type TargetPixel = T; fn line_slice(&mut self, line_number: usize) -> &mut [Self::TargetPixel] { let offset = line_number * self.pixel_stride; &mut self.data[offset..offset + self.pixel_stride] } fn num_lines(&self) -> usize { self.data.len() / self.pixel_stride } } /// A Renderer that do the rendering in software /// /// The renderer can remember what items needs to be redrawn from the previous iteration. /// /// There are two kind of possible rendering /// 1. Using [`render()`](Self::render()) to render the window in a buffer /// 2. Using [`render_by_line()`](Self::render()) to render the window line by line. This /// is only useful if the device does not have enough memory to render the whole window /// in one single buffer pub struct SoftwareRenderer { repaint_buffer_type: Cell, /// This is the area which was dirty on the previous frame. /// Only used if repaint_buffer_type == RepaintBufferType::SwappedBuffers prev_frame_dirty: Cell, partial_rendering_state: PartialRenderingState, maybe_window_adapter: RefCell>>, rotation: Cell, rendering_metrics_collector: Option>, } impl Default for SoftwareRenderer { fn default() -> Self { Self { partial_rendering_state: Default::default(), prev_frame_dirty: Default::default(), maybe_window_adapter: Default::default(), rotation: Default::default(), rendering_metrics_collector: RenderingMetricsCollector::new("software"), repaint_buffer_type: Default::default(), } } } impl SoftwareRenderer { /// Create a new Renderer pub fn new() -> Self { Default::default() } /// Create a new SoftwareRenderer. /// /// The `repaint_buffer_type` parameter specify what kind of buffer are passed to [`Self::render`] pub fn new_with_repaint_buffer_type(repaint_buffer_type: RepaintBufferType) -> Self { let self_ = Self::default(); self_.repaint_buffer_type.set(repaint_buffer_type); self_ } /// Change the what kind of buffer is being passed to [`Self::render`] /// /// This may clear the internal caches pub fn set_repaint_buffer_type(&self, repaint_buffer_type: RepaintBufferType) { if self.repaint_buffer_type.replace(repaint_buffer_type) != repaint_buffer_type { self.partial_rendering_state.clear_cache(); } } /// Returns the kind of buffer that must be passed to [`Self::render`] pub fn repaint_buffer_type(&self) -> RepaintBufferType { self.repaint_buffer_type.get() } /// Set how the window need to be rotated in the buffer. /// /// This is typically used to implement screen rotation in software pub fn set_rendering_rotation(&self, rotation: RenderingRotation) { self.rotation.set(rotation) } /// Return the current rotation. See [`Self::set_rendering_rotation()`] pub fn rendering_rotation(&self) -> RenderingRotation { self.rotation.get() } /// Render the window to the given frame buffer. /// /// The renderer uses a cache internally and will only render the part of the window /// which are dirty. The `extra_draw_region` is an extra region which will also /// be rendered. (eg: the previous dirty region in case of double buffering) /// This function returns the region that was rendered. /// /// The pixel_stride is the size (in pixels) between two lines in the buffer. /// It is equal `width` if the screen is not rotated, and `height` if the screen is rotated by 90°. /// The buffer needs to be big enough to contain the window, so its size must be at least /// `pixel_stride * height`, or `pixel_stride * width` if the screen is rotated by 90°. /// /// Returns the physical dirty region for this frame, excluding the extra_draw_region, /// in the window frame of reference. It is affected by the screen rotation. pub fn render(&self, buffer: &mut [impl TargetPixel], pixel_stride: usize) -> PhysicalRegion { self.render_buffer_impl(&mut TargetPixelSlice { data: buffer, pixel_stride }) } /// Render the window to the given frame buffer. /// /// The renderer uses a cache internally and will only render the part of the window /// which are dirty. The `extra_draw_region` is an extra region which will also /// be rendered. (eg: the previous dirty region in case of double buffering) /// This function returns the region that was rendered. /// /// The buffer's line slices need to be wide enough to if the `width` of the screen and the line count the `height`, /// or the `height` and `width` swapped if the screen is rotated by 90°. /// /// Returns the physical dirty region for this frame, excluding the extra_draw_region, /// in the window frame of reference. It is affected by the screen rotation. #[cfg(feature = "experimental")] pub fn render_into_buffer(&self, buffer: &mut impl TargetPixelBuffer) -> PhysicalRegion { self.render_buffer_impl(buffer) } fn render_buffer_impl( &self, buffer: &mut impl target_pixel_buffer::TargetPixelBuffer, ) -> PhysicalRegion { let pixels_per_line = buffer.line_slice(0).len(); let num_lines = buffer.num_lines(); let buffer_pixel_count = num_lines * pixels_per_line; let Some(window) = self.maybe_window_adapter.borrow().as_ref().and_then(|w| w.upgrade()) else { return Default::default(); }; let window_inner = WindowInner::from_pub(window.window()); let factor = ScaleFactor::new(window_inner.scale_factor()); let rotation = self.rotation.get(); let (size, background) = if let Some(window_item) = window_inner.window_item().as_ref().map(|item| item.as_pin_ref()) { ( (LogicalSize::from_lengths(window_item.width(), window_item.height()).cast() * factor) .cast(), window_item.background(), ) } else if rotation.is_transpose() { (euclid::size2(num_lines as _, pixels_per_line as _), Brush::default()) } else { (euclid::size2(pixels_per_line as _, num_lines as _), Brush::default()) }; if size.is_empty() { return Default::default(); } assert!( if rotation.is_transpose() { pixels_per_line >= size.height as usize && buffer_pixel_count >= (size.width as usize * pixels_per_line + size.height as usize) - pixels_per_line } else { pixels_per_line >= size.width as usize && buffer_pixel_count >= (size.height as usize * pixels_per_line + size.width as usize) - pixels_per_line }, "buffer of size {} with {pixels_per_line} pixels per line is too small to handle a window of size {size:?}", buffer_pixel_count ); let buffer_renderer = SceneBuilder::new( size, factor, window_inner, RenderToBuffer { buffer, dirty_range_cache: vec![], dirty_region: Default::default() }, rotation, ); let mut renderer = self.partial_rendering_state.create_partial_renderer(buffer_renderer); let window_adapter = renderer.window_adapter.clone(); window_inner .draw_contents(|components| { let logical_size = (size.cast() / factor).cast(); let dirty_region_of_existing_buffer = match self.repaint_buffer_type.get() { RepaintBufferType::NewBuffer => { Some(LogicalRect::from_size(logical_size).into()) } RepaintBufferType::ReusedBuffer => None, RepaintBufferType::SwappedBuffers => Some(self.prev_frame_dirty.take()), }; let dirty_region_for_this_frame = self.partial_rendering_state.apply_dirty_region( &mut renderer, components, logical_size, dirty_region_of_existing_buffer, ); if self.repaint_buffer_type.get() == RepaintBufferType::SwappedBuffers { self.prev_frame_dirty.set(dirty_region_for_this_frame); } let rotation = RotationInfo { orientation: rotation, screen_size: size }; let screen_rect = PhysicalRect::from_size(size); let mut i = renderer.dirty_region.iter().filter_map(|r| { (r.cast() * factor) .to_rect() .round_out() .cast() .intersection(&screen_rect)? .transformed(rotation) .into() }); let dirty_region = PhysicalRegion { rectangles: core::array::from_fn(|_| i.next().unwrap_or_default().to_box2d()), count: renderer.dirty_region.iter().count(), }; drop(i); renderer.actual_renderer.processor.dirty_region = dirty_region.clone(); if !renderer .actual_renderer .processor .buffer .fill_background(&background, &dirty_region) { let mut bg = TargetPixel::background(); // TODO: gradient background TargetPixel::blend(&mut bg, background.color().into()); renderer.actual_renderer.processor.foreach_ranges( &dirty_region.bounding_rect(), |_, buffer, _, _| { buffer.fill(bg); }, ); } for (component, origin) in components { crate::item_rendering::render_component_items( component, &mut renderer, *origin, &window_adapter, ); } if let Some(metrics) = &self.rendering_metrics_collector { metrics.measure_frame_rendered(&mut renderer); if metrics.refresh_mode() == RefreshMode::FullSpeed { self.partial_rendering_state.force_screen_refresh(); } } dirty_region }) .unwrap_or_default() } /// Render the window, line by line, into the line buffer provided by the [`LineBufferProvider`]. /// /// The renderer uses a cache internally and will only render the part of the window /// which are dirty, depending on the dirty tracking policy set in [`SoftwareRenderer::new`] /// This function returns the physical region that was rendered considering the rotation. /// /// The [`LineBufferProvider::process_line()`] function will be called for each line and should /// provide a buffer to draw into. /// /// As an example, let's imagine we want to render into a plain buffer. /// (You wouldn't normally use `render_by_line` for that because the [`Self::render`] would /// then be more efficient) /// /// ```rust /// # use i_slint_core::software_renderer::{LineBufferProvider, SoftwareRenderer, Rgb565Pixel}; /// # fn xxx<'a>(the_frame_buffer: &'a mut [Rgb565Pixel], display_width: usize, renderer: &SoftwareRenderer) { /// struct FrameBuffer<'a>{ frame_buffer: &'a mut [Rgb565Pixel], stride: usize } /// impl<'a> LineBufferProvider for FrameBuffer<'a> { /// type TargetPixel = Rgb565Pixel; /// fn process_line( /// &mut self, /// line: usize, /// range: core::ops::Range, /// render_fn: impl FnOnce(&mut [Self::TargetPixel]), /// ) { /// let line_begin = line * self.stride; /// render_fn(&mut self.frame_buffer[line_begin..][range]); /// // The line has been rendered and there could be code here to /// // send the pixel to the display /// } /// } /// renderer.render_by_line(FrameBuffer{ frame_buffer: the_frame_buffer, stride: display_width }); /// # } /// ``` pub fn render_by_line(&self, line_buffer: impl LineBufferProvider) -> PhysicalRegion { let Some(window) = self.maybe_window_adapter.borrow().as_ref().and_then(|w| w.upgrade()) else { return Default::default(); }; let window_inner = WindowInner::from_pub(window.window()); let component_rc = window_inner.component(); let component = crate::item_tree::ItemTreeRc::borrow_pin(&component_rc); if let Some(window_item) = crate::items::ItemRef::downcast_pin::( component.as_ref().get_item_ref(0), ) { let factor = ScaleFactor::new(window_inner.scale_factor()); let size = LogicalSize::from_lengths(window_item.width(), window_item.height()).cast() * factor; render_window_frame_by_line( window_inner, window_item.background(), size.cast(), self, line_buffer, ) } else { PhysicalRegion { ..Default::default() } } } } #[doc(hidden)] impl RendererSealed for SoftwareRenderer { fn text_size( &self, font_request: crate::graphics::FontRequest, text: &str, max_width: Option, scale_factor: ScaleFactor, text_wrap: TextWrap, ) -> LogicalSize { fonts::text_size(font_request, text, max_width, scale_factor, text_wrap) } fn font_metrics( &self, font_request: crate::graphics::FontRequest, scale_factor: ScaleFactor, ) -> crate::items::FontMetrics { fonts::font_metrics(font_request, scale_factor) } fn text_input_byte_offset_for_position( &self, text_input: Pin<&crate::items::TextInput>, pos: LogicalPoint, font_request: crate::graphics::FontRequest, scale_factor: ScaleFactor, ) -> usize { let visual_representation = text_input.visual_representation(None); let font = fonts::match_font(&font_request, scale_factor); let width = (text_input.width().cast() * scale_factor).cast(); let height = (text_input.height().cast() * scale_factor).cast(); let pos = (pos.cast() * scale_factor) .clamp(euclid::point2(0., 0.), euclid::point2(i16::MAX, i16::MAX).cast()) .cast(); match font { fonts::Font::PixelFont(pf) => { let layout = fonts::text_layout_for_font(&pf, &font_request, scale_factor); let paragraph = TextParagraphLayout { string: &visual_representation.text, layout, max_width: width, max_height: height, horizontal_alignment: text_input.horizontal_alignment(), vertical_alignment: text_input.vertical_alignment(), wrap: text_input.wrap(), overflow: TextOverflow::Clip, single_line: false, }; visual_representation.map_byte_offset_from_byte_offset_in_visual_text( paragraph.byte_offset_for_position((pos.x_length(), pos.y_length())), ) } #[cfg(feature = "software-renderer-systemfonts")] fonts::Font::VectorFont(vf) => { let layout = fonts::text_layout_for_font(&vf, &font_request, scale_factor); let paragraph = TextParagraphLayout { string: &visual_representation.text, layout, max_width: width, max_height: height, horizontal_alignment: text_input.horizontal_alignment(), vertical_alignment: text_input.vertical_alignment(), wrap: text_input.wrap(), overflow: TextOverflow::Clip, single_line: false, }; visual_representation.map_byte_offset_from_byte_offset_in_visual_text( paragraph.byte_offset_for_position((pos.x_length(), pos.y_length())), ) } } } fn text_input_cursor_rect_for_byte_offset( &self, text_input: Pin<&crate::items::TextInput>, byte_offset: usize, font_request: crate::graphics::FontRequest, scale_factor: ScaleFactor, ) -> LogicalRect { let visual_representation = text_input.visual_representation(None); let font = fonts::match_font(&font_request, scale_factor); let width = (text_input.width().cast() * scale_factor).cast(); let height = (text_input.height().cast() * scale_factor).cast(); let (cursor_position, cursor_height) = match font { fonts::Font::PixelFont(pf) => { let layout = fonts::text_layout_for_font(&pf, &font_request, scale_factor); let paragraph = TextParagraphLayout { string: &visual_representation.text, layout, max_width: width, max_height: height, horizontal_alignment: text_input.horizontal_alignment(), vertical_alignment: text_input.vertical_alignment(), wrap: text_input.wrap(), overflow: TextOverflow::Clip, single_line: false, }; (paragraph.cursor_pos_for_byte_offset(byte_offset), pf.height()) } #[cfg(feature = "software-renderer-systemfonts")] fonts::Font::VectorFont(vf) => { let layout = fonts::text_layout_for_font(&vf, &font_request, scale_factor); let paragraph = TextParagraphLayout { string: &visual_representation.text, layout, max_width: width, max_height: height, horizontal_alignment: text_input.horizontal_alignment(), vertical_alignment: text_input.vertical_alignment(), wrap: text_input.wrap(), overflow: TextOverflow::Clip, single_line: false, }; (paragraph.cursor_pos_for_byte_offset(byte_offset), vf.height()) } }; (PhysicalRect::new( PhysicalPoint::from_lengths(cursor_position.0, cursor_position.1), PhysicalSize::from_lengths( (text_input.text_cursor_width().cast() * scale_factor).cast(), cursor_height, ), ) .cast() / scale_factor) .cast() } fn free_graphics_resources( &self, _component: crate::item_tree::ItemTreeRef, items: &mut dyn Iterator>>, ) -> Result<(), crate::platform::PlatformError> { self.partial_rendering_state.free_graphics_resources(items); Ok(()) } fn mark_dirty_region(&self, region: crate::item_rendering::DirtyRegion) { self.partial_rendering_state.mark_dirty_region(region); } fn register_bitmap_font(&self, font_data: &'static crate::graphics::BitmapFont) { fonts::register_bitmap_font(font_data); } #[cfg(feature = "software-renderer-systemfonts")] fn register_font_from_memory( &self, data: &'static [u8], ) -> Result<(), std::boxed::Box> { self::fonts::systemfonts::register_font_from_memory(data) } #[cfg(all(feature = "software-renderer-systemfonts", not(target_arch = "wasm32")))] fn register_font_from_path( &self, path: &std::path::Path, ) -> Result<(), std::boxed::Box> { self::fonts::systemfonts::register_font_from_path(path) } fn default_font_size(&self) -> LogicalLength { self::fonts::DEFAULT_FONT_SIZE } fn set_window_adapter(&self, window_adapter: &Rc) { *self.maybe_window_adapter.borrow_mut() = Some(Rc::downgrade(window_adapter)); self.partial_rendering_state.clear_cache(); } fn take_snapshot(&self) -> Result, PlatformError> { let Some(window_adapter) = self.maybe_window_adapter.borrow().as_ref().and_then(|w| w.upgrade()) else { return Err( "SoftwareRenderer's screenshot called without a window adapter present".into() ); }; let window = window_adapter.window(); let size = window.size(); let Some((width, height)) = size.width.try_into().ok().zip(size.height.try_into().ok()) else { // Nothing to render return Err("take_snapshot() called on window with invalid size".into()); }; let mut target_buffer = SharedPixelBuffer::::new(width, height); self.set_repaint_buffer_type(RepaintBufferType::NewBuffer); self.render(target_buffer.make_mut_slice(), width as usize); // ensure that caches are clear for the next call self.set_repaint_buffer_type(RepaintBufferType::NewBuffer); let mut target_buffer_with_alpha = SharedPixelBuffer::::new(target_buffer.width(), target_buffer.height()); for (target_pixel, source_pixel) in target_buffer_with_alpha .make_mut_slice() .iter_mut() .zip(target_buffer.as_slice().iter()) { *target_pixel.rgb_mut() = *source_pixel; } Ok(target_buffer_with_alpha) } } fn render_window_frame_by_line( window: &WindowInner, background: Brush, size: PhysicalSize, renderer: &SoftwareRenderer, mut line_buffer: impl LineBufferProvider, ) -> PhysicalRegion { let mut scene = prepare_scene(window, size, renderer); let to_draw_tr = scene.dirty_region.bounding_rect(); let mut background_color = TargetPixel::background(); // FIXME gradient TargetPixel::blend(&mut background_color, background.color().into()); while scene.current_line < to_draw_tr.origin.y_length() + to_draw_tr.size.height_length() { for r in &scene.current_line_ranges { line_buffer.process_line( scene.current_line.get() as usize, r.start as usize..r.end as usize, |line_buffer| { let offset = r.start; line_buffer.fill(background_color); for span in scene.items[0..scene.current_items_index].iter().rev() { debug_assert!(scene.current_line >= span.pos.y_length()); debug_assert!( scene.current_line < span.pos.y_length() + span.size.height_length(), ); if span.pos.x >= r.end { continue; } let begin = r.start.max(span.pos.x); let end = r.end.min(span.pos.x + span.size.width); if begin >= end { continue; } let extra_left_clip = begin - span.pos.x; let extra_right_clip = span.pos.x + span.size.width - end; let range_buffer = &mut line_buffer[(begin - offset) as usize..(end - offset) as usize]; match span.command { SceneCommand::Rectangle { color } => { TargetPixel::blend_slice(range_buffer, color); } SceneCommand::Texture { texture_index } => { let texture = &scene.vectors.textures[texture_index as usize]; draw_functions::draw_texture_line( &PhysicalRect { origin: span.pos, size: span.size }, scene.current_line, texture, range_buffer, extra_left_clip, extra_right_clip, ); } SceneCommand::SharedBuffer { shared_buffer_index } => { let texture = scene.vectors.shared_buffers [shared_buffer_index as usize] .as_texture(); draw_functions::draw_texture_line( &PhysicalRect { origin: span.pos, size: span.size }, scene.current_line, &texture, range_buffer, extra_left_clip, extra_right_clip, ); } SceneCommand::RoundedRectangle { rectangle_index } => { let rr = &scene.vectors.rounded_rectangles[rectangle_index as usize]; draw_functions::draw_rounded_rectangle_line( &PhysicalRect { origin: span.pos, size: span.size }, scene.current_line, rr, range_buffer, extra_left_clip, extra_right_clip, ); } SceneCommand::Gradient { gradient_index } => { let g = &scene.vectors.gradients[gradient_index as usize]; draw_functions::draw_gradient_line( &PhysicalRect { origin: span.pos, size: span.size }, scene.current_line, g, range_buffer, extra_left_clip, ); } } } }, ); } if scene.current_line < to_draw_tr.origin.y_length() + to_draw_tr.size.height_length() { scene.next_line(); } } scene.dirty_region } fn prepare_scene( window: &WindowInner, size: PhysicalSize, software_renderer: &SoftwareRenderer, ) -> Scene { let factor = ScaleFactor::new(window.scale_factor()); let prepare_scene = SceneBuilder::new( size, factor, window, PrepareScene::default(), software_renderer.rotation.get(), ); let mut renderer = software_renderer.partial_rendering_state.create_partial_renderer(prepare_scene); let window_adapter = renderer.window_adapter.clone(); let mut dirty_region = PhysicalRegion::default(); window.draw_contents(|components| { let logical_size = (size.cast() / factor).cast(); let dirty_region_of_existing_buffer = match software_renderer.repaint_buffer_type.get() { RepaintBufferType::NewBuffer => Some(LogicalRect::from_size(logical_size).into()), RepaintBufferType::ReusedBuffer => None, RepaintBufferType::SwappedBuffers => Some(software_renderer.prev_frame_dirty.take()), }; let dirty_region_for_this_frame = software_renderer.partial_rendering_state.apply_dirty_region( &mut renderer, components, logical_size, dirty_region_of_existing_buffer, ); if software_renderer.repaint_buffer_type.get() == RepaintBufferType::SwappedBuffers { software_renderer.prev_frame_dirty.set(dirty_region_for_this_frame); } let rotation = RotationInfo { orientation: software_renderer.rotation.get(), screen_size: size }; let screen_rect = PhysicalRect::from_size(size); let mut i = renderer.dirty_region.iter().filter_map(|r| { (r.cast() * factor) .to_rect() .round_out() .cast() .intersection(&screen_rect)? .transformed(rotation) .into() }); dirty_region = PhysicalRegion { rectangles: core::array::from_fn(|_| i.next().unwrap_or_default().to_box2d()), count: renderer.dirty_region.iter().count(), }; drop(i); for (component, origin) in components { crate::item_rendering::render_component_items( component, &mut renderer, *origin, &window_adapter, ); } }); if let Some(metrics) = &software_renderer.rendering_metrics_collector { metrics.measure_frame_rendered(&mut renderer); if metrics.refresh_mode() == RefreshMode::FullSpeed { software_renderer.partial_rendering_state.force_screen_refresh(); } } let prepare_scene = renderer.into_inner(); /* // visualize dirty regions let mut prepare_scene = prepare_scene; for rect in dirty_region.iter() { prepare_scene.processor.process_rounded_rectangle( euclid::rect(rect.0.x as _, rect.0.y as _, rect.1.width as _, rect.1.height as _), RoundedRectangle { radius: BorderRadius::default(), width: Length::new(1), border_color: Color::from_argb_u8(128, 255, 0, 0).into(), inner_color: PremultipliedRgbaColor::default(), left_clip: Length::default(), right_clip: Length::default(), top_clip: Length::default(), bottom_clip: Length::default(), }, ) } // */ Scene::new(prepare_scene.processor.items, prepare_scene.processor.vectors, dirty_region) } trait ProcessScene { fn process_scene_texture(&mut self, geometry: PhysicalRect, texture: SceneTexture<'static>); fn process_target_texture( &mut self, texture: &target_pixel_buffer::DrawTextureArgs, clip: PhysicalRect, ); fn process_rectangle(&mut self, _: &target_pixel_buffer::DrawRectangleArgs, clip: PhysicalRect); fn process_simple_rectangle(&mut self, geometry: PhysicalRect, color: PremultipliedRgbaColor); fn process_rounded_rectangle(&mut self, geometry: PhysicalRect, data: RoundedRectangle); fn process_gradient(&mut self, geometry: PhysicalRect, gradient: GradientCommand); } fn process_rectangle_impl( processor: &mut dyn ProcessScene, args: &target_pixel_buffer::DrawRectangleArgs, clip: &PhysicalRect, ) { let geom = args.geometry(); let Some(clipped) = geom.intersection(&clip.cast()) else { return }; let color = if let Brush::LinearGradient(g) = &args.background { let angle = g.angle() + args.rotation.angle(); let tan = angle.to_radians().tan().abs(); let start = if !tan.is_finite() { 255. } else { let h = tan * geom.width(); 255. * h / (h + geom.height()) } as u8; let mut angle = angle as i32 % 360; if angle < 0 { angle += 360; } let mut stops = g .stops() .copied() .map(|mut s| { s.color = alpha_color(s.color, args.alpha); s }) .peekable(); let mut idx = 0; let stop_count = g.stops().count(); while let (Some(mut s1), Some(mut s2)) = (stops.next(), stops.peek().copied()) { let mut flags = 0; if (angle % 180) > 90 { flags |= 0b1; } if angle <= 90 || angle > 270 { core::mem::swap(&mut s1, &mut s2); s1.position = 1. - s1.position; s2.position = 1. - s2.position; if idx == 0 { flags |= 0b100; } if idx == stop_count - 2 { flags |= 0b010; } } else { if idx == 0 { flags |= 0b010; } if idx == stop_count - 2 { flags |= 0b100; } } idx += 1; let (adjust_left, adjust_right) = if (angle % 180) > 90 { ( (geom.width() * s1.position).floor() as i16, (geom.width() * (1. - s2.position)).ceil() as i16, ) } else { ( (geom.width() * (1. - s2.position)).ceil() as i16, (geom.width() * s1.position).floor() as i16, ) }; let gr = GradientCommand { color1: s1.color.into(), color2: s2.color.into(), start, flags, top_clip: Length::new( (clipped.min_y() - geom.min_y() - (geom.height() * s1.position).floor()) as i16, ), bottom_clip: Length::new( (geom.max_y() - clipped.max_y() - (geom.height() * (1. - s2.position)).ceil()) as i16, ), left_clip: Length::new((clipped.min_x() - geom.min_x()) as i16 - adjust_left), right_clip: Length::new((geom.max_x() - clipped.max_x()) as i16 - adjust_right), }; let act_rect = clipped.round().cast(); let size_y = act_rect.height_length() + gr.top_clip + gr.bottom_clip; let size_x = act_rect.width_length() + gr.left_clip + gr.right_clip; if size_x.get() == 0 || size_y.get() == 0 { // the position are too close to each other // FIXME: For the first or the last, we should draw a plain color to the end continue; } processor.process_gradient(act_rect, gr); } Color::default() } else { alpha_color(args.background.color(), args.alpha) }; let mut border_color = PremultipliedRgbaColor::from(alpha_color(args.border.color(), args.alpha)); let color = PremultipliedRgbaColor::from(color); let mut border = PhysicalLength::new(args.border_width as _); if border_color.alpha == 0 { border = PhysicalLength::new(0); } else if border_color.alpha < 255 { // Find a color for the border which is an equivalent to blend the background and then the border. // In the end, the resulting of blending the background and the color is // (A + B) + C, where A is the buffer color, B is the background, and C is the border. // which expands to (A*(1-Bα) + B*Bα)*(1-Cα) + C*Cα = A*(1-(Bα+Cα-Bα*Cα)) + B*Bα*(1-Cα) + C*Cα // so let the new alpha be: Nα = Bα+Cα-Bα*Cα, then this is A*(1-Nα) + N*Nα // with N = (B*Bα*(1-Cα) + C*Cα)/Nα // N being the equivalent color of the border that mixes the background and the border // In pre-multiplied space, the formula simplifies further N' = B'*(1-Cα) + C' let b = border_color; let b_alpha_16 = b.alpha as u16; border_color = PremultipliedRgbaColor { red: ((color.red as u16 * (255 - b_alpha_16)) / 255) as u8 + b.red, green: ((color.green as u16 * (255 - b_alpha_16)) / 255) as u8 + b.green, blue: ((color.blue as u16 * (255 - b_alpha_16)) / 255) as u8 + b.blue, alpha: (color.alpha as u16 + b_alpha_16 - (color.alpha as u16 * b_alpha_16) / 255) as u8, } } let radius = PhysicalBorderRadius { top_left: args.top_left_radius as _, top_right: args.top_right_radius as _, bottom_right: args.bottom_right_radius as _, bottom_left: args.bottom_left_radius as _, _unit: Default::default(), }; if !radius.is_zero() { // Add a small value to make sure that the clip is always positive despite floating point shenanigans const E: f32 = 0.00001; processor.process_rounded_rectangle( clipped.round().cast(), RoundedRectangle { radius, width: border, border_color, inner_color: color, top_clip: PhysicalLength::new((clipped.min_y() - geom.min_y() + E) as _), bottom_clip: PhysicalLength::new((geom.max_y() - clipped.max_y() + E) as _), left_clip: PhysicalLength::new((clipped.min_x() - geom.min_x() + E) as _), right_clip: PhysicalLength::new((geom.max_x() - clipped.max_x() + E) as _), }, ); return; } if color.alpha > 0 { if let Some(r) = geom.round().cast().inflate(-border.get(), -border.get()).intersection(clip) { processor.process_simple_rectangle(r, color); } } if border_color.alpha > 0 { let mut add_border = |r: PhysicalRect| { if let Some(r) = r.intersection(clip) { processor.process_simple_rectangle(r, border_color); } }; let b = border.get(); let g = geom.round().cast(); add_border(euclid::rect(g.min_x(), g.min_y(), g.width(), b)); add_border(euclid::rect(g.min_x(), g.min_y() + g.height() - b, g.width(), b)); add_border(euclid::rect(g.min_x(), g.min_y() + b, b, g.height() - b - b)); add_border(euclid::rect(g.min_x() + g.width() - b, g.min_y() + b, b, g.height() - b - b)); } } struct RenderToBuffer<'a, TargetPixelBuffer> { buffer: &'a mut TargetPixelBuffer, dirty_range_cache: Vec>, dirty_region: PhysicalRegion, } impl RenderToBuffer<'_, B> { fn foreach_ranges( &mut self, geometry: &PhysicalRect, mut f: impl FnMut(i16, &mut [B::TargetPixel], i16, i16), ) { self.foreach_region(geometry, |buffer, rect, extra_left_clip, extra_right_clip| { for l in rect.y_range() { f( l, &mut buffer.line_slice(l as usize) [rect.min_x() as usize..rect.max_x() as usize], extra_left_clip, extra_right_clip, ); } }); } fn foreach_region( &mut self, geometry: &PhysicalRect, mut f: impl FnMut(&mut B, PhysicalRect, i16, i16), ) { let mut line = geometry.min_y(); while let Some(mut next) = region_line_ranges(&self.dirty_region, line, &mut self.dirty_range_cache) { next = next.min(geometry.max_y()); for r in &self.dirty_range_cache { if geometry.origin.x >= r.end { continue; } let begin = r.start.max(geometry.origin.x); let end = r.end.min(geometry.origin.x + geometry.size.width); if begin >= end { continue; } let extra_left_clip = begin - geometry.origin.x; let extra_right_clip = geometry.origin.x + geometry.size.width - end; let region = PhysicalRect { origin: PhysicalPoint::new(begin, line), size: PhysicalSize::new(end - begin, next - line), }; f(&mut self.buffer, region, extra_left_clip, extra_right_clip); } if next == geometry.max_y() { break; } line = next; } } fn process_texture_impl(&mut self, geometry: PhysicalRect, texture: SceneTexture<'_>) { self.foreach_ranges(&geometry, |line, buffer, extra_left_clip, extra_right_clip| { draw_functions::draw_texture_line( &geometry, PhysicalLength::new(line), &texture, buffer, extra_left_clip, extra_right_clip, ); }); } } impl ProcessScene for RenderToBuffer<'_, B> { fn process_scene_texture(&mut self, geometry: PhysicalRect, texture: SceneTexture<'static>) { self.process_texture_impl(geometry, texture); } fn process_target_texture( &mut self, texture: &target_pixel_buffer::DrawTextureArgs, clip: PhysicalRect, ) { if self.buffer.draw_texture(texture, &self.dirty_region.intersection(&clip)) { return; } let Some((texture, geometry)) = SceneTexture::from_target_texture(texture, &clip) else { return; }; self.process_texture_impl(geometry, texture); } fn process_rectangle( &mut self, args: &target_pixel_buffer::DrawRectangleArgs, clip: PhysicalRect, ) { if self.buffer.draw_rectangle(args, &self.dirty_region.intersection(&clip)) { return; } process_rectangle_impl(self, args, &clip); } fn process_rounded_rectangle(&mut self, geometry: PhysicalRect, rr: RoundedRectangle) { self.foreach_ranges(&geometry, |line, buffer, extra_left_clip, extra_right_clip| { draw_functions::draw_rounded_rectangle_line( &geometry, PhysicalLength::new(line), &rr, buffer, extra_left_clip, extra_right_clip, ); }); } fn process_simple_rectangle(&mut self, geometry: PhysicalRect, color: PremultipliedRgbaColor) { self.foreach_ranges(&geometry, |_line, buffer, _extra_left_clip, _extra_right_clip| { ::blend_slice(buffer, color) }); } fn process_gradient(&mut self, geometry: PhysicalRect, g: GradientCommand) { self.foreach_ranges(&geometry, |line, buffer, extra_left_clip, _extra_right_clip| { draw_functions::draw_gradient_line( &geometry, PhysicalLength::new(line), &g, buffer, extra_left_clip, ); }); } } #[derive(Default)] struct PrepareScene { items: Vec, vectors: SceneVectors, } impl ProcessScene for PrepareScene { fn process_scene_texture(&mut self, geometry: PhysicalRect, texture: SceneTexture<'static>) { let texture_index = self.vectors.textures.len() as u16; self.vectors.textures.push(texture); self.items.push(SceneItem { pos: geometry.origin, size: geometry.size, z: self.items.len() as u16, command: SceneCommand::Texture { texture_index }, }); } fn process_target_texture( &mut self, texture: &target_pixel_buffer::DrawTextureArgs, clip: PhysicalRect, ) { let Some((extra, geometry)) = SceneTextureExtra::from_target_texture(texture, &clip) else { return; }; match &texture.data { target_pixel_buffer::TextureDataContainer::Static(texture_data) => { let texture_index = self.vectors.textures.len() as u16; let pixel_stride = (texture_data.byte_stride / texture_data.pixel_format.bpp()) as u16; self.vectors.textures.push(SceneTexture { data: texture_data.data, format: texture_data.pixel_format, pixel_stride, extra, }); self.items.push(SceneItem { pos: geometry.origin, size: geometry.size, z: self.items.len() as u16, command: SceneCommand::Texture { texture_index }, }); } target_pixel_buffer::TextureDataContainer::Shared { buffer, source_rect } => { let shared_buffer_index = self.vectors.shared_buffers.len() as u16; self.vectors.shared_buffers.push(SharedBufferCommand { buffer: buffer.clone(), source_rect: *source_rect, extra, }); self.items.push(SceneItem { pos: geometry.origin, size: geometry.size, z: self.items.len() as u16, command: SceneCommand::SharedBuffer { shared_buffer_index }, }); } } } fn process_rectangle( &mut self, args: &target_pixel_buffer::DrawRectangleArgs, clip: PhysicalRect, ) { process_rectangle_impl(self, args, &clip); } fn process_simple_rectangle(&mut self, geometry: PhysicalRect, color: PremultipliedRgbaColor) { let size = geometry.size; if !size.is_empty() { let z = self.items.len() as u16; let pos = geometry.origin; self.items.push(SceneItem { pos, size, z, command: SceneCommand::Rectangle { color } }); } } fn process_rounded_rectangle(&mut self, geometry: PhysicalRect, data: RoundedRectangle) { let size = geometry.size; if !size.is_empty() { let rectangle_index = self.vectors.rounded_rectangles.len() as u16; self.vectors.rounded_rectangles.push(data); self.items.push(SceneItem { pos: geometry.origin, size, z: self.items.len() as u16, command: SceneCommand::RoundedRectangle { rectangle_index }, }); } } fn process_gradient(&mut self, geometry: PhysicalRect, gradient: GradientCommand) { let size = geometry.size; if !size.is_empty() { let gradient_index = self.vectors.gradients.len() as u16; self.vectors.gradients.push(gradient); self.items.push(SceneItem { pos: geometry.origin, size, z: self.items.len() as u16, command: SceneCommand::Gradient { gradient_index }, }); } } } struct SceneBuilder<'a, T> { processor: T, state_stack: Vec, current_state: RenderState, scale_factor: ScaleFactor, window: &'a WindowInner, rotation: RotationInfo, } impl<'a, T: ProcessScene> SceneBuilder<'a, T> { fn new( screen_size: PhysicalSize, scale_factor: ScaleFactor, window: &'a WindowInner, processor: T, orientation: RenderingRotation, ) -> Self { Self { processor, state_stack: vec![], current_state: RenderState { alpha: 1., offset: LogicalPoint::default(), clip: LogicalRect::new( LogicalPoint::default(), (screen_size.cast() / scale_factor).cast(), ), }, scale_factor, window, rotation: RotationInfo { orientation, screen_size }, } } fn should_draw(&self, rect: &LogicalRect) -> bool { !rect.size.is_empty() && self.current_state.alpha > 0.01 && self.current_state.clip.intersects(rect) } fn draw_image_impl( &mut self, image_inner: &ImageInner, crate::graphics::FitResult { clip_rect: source_rect, source_to_target_x, source_to_target_y, size: fit_size, offset: image_fit_offset, tiled, }: crate::graphics::FitResult, colorize: Color, ) { let global_alpha_u16 = (self.current_state.alpha * 255.) as u16; let offset = self.current_state.offset.cast() * self.scale_factor + image_fit_offset.to_vector(); let physical_clip = (self.current_state.clip.translate(self.current_state.offset.to_vector()).cast() * self.scale_factor) .round() .cast() .transformed(self.rotation); match image_inner { ImageInner::None => (), ImageInner::StaticTextures(StaticTextures { data, textures, size, original_size, .. }) => { let adjust_x = size.width as f32 / original_size.width as f32; let adjust_y = size.height as f32 / original_size.height as f32; let source_to_target_x = source_to_target_x / adjust_x; let source_to_target_y = source_to_target_y / adjust_y; let source_rect = source_rect.cast::().scale(adjust_x, adjust_y).round().to_box2d().cast(); for t in textures.as_slice() { let t_rect = t.rect.to_box2d(); // That's the source rect in the whole image coordinate let Some(src_rect) = t_rect.intersection(&source_rect) else { continue }; let target_rect = if tiled.is_some() { euclid::Rect::new(offset, fit_size).round().cast::() } else { // map t.rect to to the target euclid::Rect::::from_untyped( &src_rect.to_rect().translate(-source_rect.min.to_vector()).cast(), ) .scale(source_to_target_x, source_to_target_y) .translate(offset.to_vector()) .round() .cast::() }; let target_rect = target_rect.transformed(self.rotation).round(); let Some(clipped_target) = physical_clip.intersection(&target_rect) else { continue; }; let pixel_stride = t.rect.width() as usize; let core::ops::Range { start, end } = compute_range_in_buffer( &PhysicalRect::from_untyped( &src_rect.to_rect().translate(-t.rect.origin.to_vector()).cast(), ), pixel_stride, ); let bpp = t.format.bpp(); let color = if colorize.alpha() > 0 { colorize } else { t.color }; let alpha = if colorize.alpha() > 0 || t.format == TexturePixelFormat::AlphaMap { color.alpha() as u16 * global_alpha_u16 / 255 } else { global_alpha_u16 } as u8; let tiling = tiled.map(|tile_o| { let src_o = src_rect.min - source_rect.min; let gap = (src_o) + (source_rect.max - src_rect.max); target_pixel_buffer::TilingInfo { offset_x: ((src_o.x as f32 - tile_o.x as f32) * source_to_target_x) .round() as _, offset_y: ((src_o.y as f32 - tile_o.y as f32) * source_to_target_y) .round() as _, scale_x: 1. / source_to_target_x, scale_y: 1. / source_to_target_y, gap_x: (gap.x as f32 * source_to_target_x).round() as _, gap_y: (gap.y as f32 * source_to_target_y).round() as _, } }); let t = target_pixel_buffer::DrawTextureArgs { data: target_pixel_buffer::TextureDataContainer::Static( target_pixel_buffer::TextureData::new( &data.as_slice()[t.index..][start * bpp..end * bpp], t.format, pixel_stride * bpp, src_rect.size().cast(), ), ), colorize: (colorize.alpha() > 0).then_some(colorize), alpha, dst_x: target_rect.origin.x as _, dst_y: target_rect.origin.y as _, dst_width: target_rect.size.width as _, dst_height: target_rect.size.height as _, rotation: self.rotation.orientation, tiling, }; self.processor.process_target_texture(&t, clipped_target.cast()); } } ImageInner::NineSlice(..) => unreachable!(), _ => { let target_rect = euclid::Rect::new(offset, fit_size).round().cast().transformed(self.rotation); let Some(clipped_target) = physical_clip.intersection(&target_rect) else { return; }; let orig = image_inner.size().cast::(); let svg_target_size = if tiled.is_some() { euclid::size2(orig.width * source_to_target_x, orig.height * source_to_target_y) .round() .cast() } else { target_rect.size.cast() }; if let Some(buffer) = image_inner.render_to_buffer(Some(svg_target_size)) { let buf_size = buffer.size().cast::(); let alpha = if colorize.alpha() > 0 { colorize.alpha() as u16 * global_alpha_u16 / 255 } else { global_alpha_u16 } as u8; let tiling = tiled.map(|tile_o| target_pixel_buffer::TilingInfo { offset_x: (tile_o.x as f32 * -source_to_target_x).round() as _, offset_y: (tile_o.y as f32 * -source_to_target_y).round() as _, scale_x: 1. / source_to_target_x, scale_y: 1. / source_to_target_y, gap_x: 0, gap_y: 0, }); let t = target_pixel_buffer::DrawTextureArgs { data: target_pixel_buffer::TextureDataContainer::Shared { buffer: SharedBufferData::SharedImage(buffer), source_rect: PhysicalRect::from_untyped( &source_rect .cast::() .scale( buf_size.width / orig.width, buf_size.height / orig.height, ) .round() .cast(), ), }, colorize: (colorize.alpha() > 0).then_some(colorize), alpha, dst_x: target_rect.origin.x as _, dst_y: target_rect.origin.y as _, dst_width: target_rect.size.width as _, dst_height: target_rect.size.height as _, rotation: self.rotation.orientation, tiling, }; self.processor.process_target_texture(&t, clipped_target.cast()); } else { unimplemented!("The image cannot be rendered") } } }; } fn draw_text_paragraph( &mut self, paragraph: &TextParagraphLayout<'_, Font>, physical_clip: euclid::Rect, offset: euclid::Vector2D, color: Color, selection: Option, ) where Font: AbstractFont + crate::textlayout::TextShaper + GlyphRenderer, { paragraph .layout_lines::<()>( |glyphs, line_x, line_y, _, sel| { let baseline_y = line_y + paragraph.layout.font.ascent(); if let (Some(sel), Some(selection)) = (sel, &selection) { let geometry = euclid::rect( line_x.get() + sel.start.get(), line_y.get(), (sel.end - sel.start).get(), paragraph.layout.font.height().get(), ); if let Some(clipped_src) = geometry.intersection(&physical_clip.cast()) { let geometry = clipped_src.translate(offset.cast()).transformed(self.rotation); let args = target_pixel_buffer::DrawRectangleArgs::from_rect( geometry.cast(), selection.selection_background.into(), ); self.processor.process_rectangle(&args, geometry); } } let scale_delta = paragraph.layout.font.scale_delta(); for positioned_glyph in glyphs { let Some(glyph) = paragraph.layout.font.render_glyph(positioned_glyph.glyph_id) else { continue; }; let gl_x = PhysicalLength::new((-glyph.x).truncate() as i16); let gl_y = PhysicalLength::new(glyph.y.truncate() as i16); let target_rect = PhysicalRect::new( PhysicalPoint::from_lengths( line_x + positioned_glyph.x - gl_x, baseline_y - gl_y - glyph.height, ), glyph.size(), ) .cast(); let color = match &selection { Some(s) if s.selection.contains(&positioned_glyph.text_byte_offset) => { s.selection_color } _ => color, }; let Some(clipped_target) = physical_clip.intersection(&target_rect) else { continue; }; let data = match &glyph.alpha_map { fonts::GlyphAlphaMap::Static(data) => { if glyph.sdf { let geometry = clipped_target.translate(offset).round(); let origin = (geometry.origin - offset.round()).round().cast::(); let off_x = origin.x - target_rect.origin.x as i16; let off_y = origin.y - target_rect.origin.y as i16; let pixel_stride = glyph.pixel_stride; let mut geometry = geometry.cast(); if geometry.size.width > glyph.width.get() - off_x { geometry.size.width = glyph.width.get() - off_x } if geometry.size.height > glyph.height.get() - off_y { geometry.size.height = glyph.height.get() - off_y } let source_size = geometry.size; if source_size.is_empty() { continue; } let delta32 = Fixed::::from_fixed(scale_delta); let normalize = |x: Fixed| { if x < Fixed::from_integer(0) { x + Fixed::from_integer(1) } else { x } }; let fract_x = normalize( (-glyph.x) - Fixed::from_integer(gl_x.get() as _), ); let off_x = delta32 * off_x as i32 + fract_x; let fract_y = normalize(glyph.y - Fixed::from_integer(gl_y.get() as _)); let off_y = delta32 * off_y as i32 + fract_y; let texture = SceneTexture { data, pixel_stride, format: TexturePixelFormat::SignedDistanceField, extra: SceneTextureExtra { colorize: color, // color already is mixed with global alpha alpha: color.alpha(), rotation: self.rotation.orientation, dx: scale_delta, dy: scale_delta, off_x: Fixed::try_from_fixed(off_x).unwrap(), off_y: Fixed::try_from_fixed(off_y).unwrap(), }, }; self.processor.process_scene_texture( geometry.transformed(self.rotation), texture, ); continue; }; target_pixel_buffer::TextureDataContainer::Static( target_pixel_buffer::TextureData::new( data, TexturePixelFormat::AlphaMap, glyph.pixel_stride as usize, euclid::size2(glyph.width.get(), glyph.height.get()).cast(), ), ) } fonts::GlyphAlphaMap::Shared(data) => { let source_rect = euclid::rect(0, 0, glyph.width.0, glyph.height.0); target_pixel_buffer::TextureDataContainer::Shared { buffer: SharedBufferData::AlphaMap { data: data.clone(), width: glyph.pixel_stride, }, source_rect, } } }; let clipped_target = clipped_target.translate(offset).round().transformed(self.rotation); let target_rect = target_rect.translate(offset).round().transformed(self.rotation); let t = target_pixel_buffer::DrawTextureArgs { data, colorize: Some(color), // color already is mixed with global alpha alpha: color.alpha(), dst_x: target_rect.origin.x as _, dst_y: target_rect.origin.y as _, dst_width: target_rect.size.width as _, dst_height: target_rect.size.height as _, rotation: self.rotation.orientation, tiling: None, }; self.processor.process_target_texture(&t, clipped_target.cast()); } core::ops::ControlFlow::Continue(()) }, selection.as_ref().map(|s| s.selection.clone()), ) .ok(); } /// Returns the color, mixed with the current_state's alpha fn alpha_color(&self, color: Color) -> Color { if self.current_state.alpha < 1.0 { Color::from_argb_u8( (color.alpha() as f32 * self.current_state.alpha) as u8, color.red(), color.green(), color.blue(), ) } else { color } } } fn alpha_color(color: Color, alpha: u8) -> Color { if alpha < 255 { Color::from_argb_u8( ((color.alpha() as u32 * alpha as u32) / 255) as u8, color.red(), color.green(), color.blue(), ) } else { color } } struct SelectionInfo { selection_color: Color, selection_background: Color, selection: core::ops::Range, } #[derive(Clone, Copy, Debug)] struct RenderState { alpha: f32, offset: LogicalPoint, clip: LogicalRect, } impl crate::item_rendering::ItemRenderer for SceneBuilder<'_, T> { fn draw_rectangle( &mut self, rect: Pin<&dyn RenderRectangle>, _: &ItemRc, size: LogicalSize, _cache: &CachedRenderingData, ) { let geom = LogicalRect::from(size); if self.should_draw(&geom) { let geom = (geom.translate(self.current_state.offset.to_vector()).cast() * self.scale_factor) .transformed(self.rotation); let clipped = (self.current_state.clip.translate(self.current_state.offset.to_vector()).cast() * self.scale_factor) .round() .cast() .transformed(self.rotation); let mut args = target_pixel_buffer::DrawRectangleArgs::from_rect(geom, rect.background()); args.alpha = (self.current_state.alpha * 255.) as u8; args.rotation = self.rotation.orientation; self.processor.process_rectangle(&args, clipped); } } fn draw_border_rectangle( &mut self, rect: Pin<&dyn RenderBorderRectangle>, _: &ItemRc, size: LogicalSize, _: &CachedRenderingData, ) { let geom = LogicalRect::from(size); if self.should_draw(&geom) { let geom = (geom.translate(self.current_state.offset.to_vector()).cast() * self.scale_factor) .transformed(self.rotation); let clipped = (self.current_state.clip.translate(self.current_state.offset.to_vector()).cast() * self.scale_factor) .round() .cast() .transformed(self.rotation); let radius = (rect.border_radius().cast() * self.scale_factor) .transformed(self.rotation) .min(BorderRadius::from_length(geom.width_length() / 2.)) .min(BorderRadius::from_length(geom.height_length() / 2.)); let border = rect.border_width().cast() * self.scale_factor; let border_color = if border.get() > 0.01 { rect.border_color() } else { Default::default() }; let args = target_pixel_buffer::DrawRectangleArgs { x: geom.origin.x, y: geom.origin.y, width: geom.size.width, height: geom.size.height, top_left_radius: radius.top_left, top_right_radius: radius.top_right, bottom_right_radius: radius.bottom_right, bottom_left_radius: radius.bottom_left, border_width: border.get(), background: rect.background(), border: border_color, alpha: (self.current_state.alpha * 255.) as u8, rotation: self.rotation.orientation, }; self.processor.process_rectangle(&args, clipped); } } fn draw_window_background( &mut self, rect: Pin<&dyn RenderRectangle>, _self_rc: &ItemRc, _size: LogicalSize, _cache: &CachedRenderingData, ) { // register a dependency for the partial renderer's dirty tracker. The actual rendering is done earlier in the software renderer. let _ = rect.background(); } fn draw_image( &mut self, image: Pin<&dyn RenderImage>, _: &ItemRc, size: LogicalSize, _: &CachedRenderingData, ) { let geom = LogicalRect::from(size); if self.should_draw(&geom) { let source = image.source(); let image_inner: &ImageInner = (&source).into(); if let ImageInner::NineSlice(nine) = image_inner { let colorize = image.colorize().color(); let source_size = source.size(); for fit in crate::graphics::fit9slice( source_size, nine.1, size.cast() * self.scale_factor, self.scale_factor, image.alignment(), image.tiling(), ) { self.draw_image_impl(&nine.0, fit, colorize); } return; } let source_clip = image.source_clip().map_or_else( || euclid::Rect::new(Default::default(), source.size().cast()), |clip| { clip.intersection(&euclid::Rect::from_size(source.size().cast())) .unwrap_or_default() }, ); let phys_size = geom.size_length().cast() * self.scale_factor; let fit = crate::graphics::fit( image.image_fit(), phys_size, source_clip, self.scale_factor, image.alignment(), image.tiling(), ); self.draw_image_impl(image_inner, fit, image.colorize().color()); } } fn draw_text( &mut self, text: Pin<&dyn crate::item_rendering::RenderText>, self_rc: &ItemRc, size: LogicalSize, _cache: &CachedRenderingData, ) { let string = text.text(); if string.trim().is_empty() { return; } let geom = LogicalRect::from(size); if !self.should_draw(&geom) { return; } let font_request = text.font_request(self_rc); let color = self.alpha_color(text.color().color()); let max_size = (geom.size.cast() * self.scale_factor).cast(); // Clip glyphs not only against the global clip but also against the Text's geometry to avoid drawing outside // of its boundaries (that breaks partial rendering and the cast to usize for the item relative coordinate below). // FIXME: we should allow drawing outside of the Text element's boundaries. let physical_clip = if let Some(logical_clip) = self.current_state.clip.intersection(&geom) { logical_clip.cast() * self.scale_factor } else { return; // This should have been caught earlier already }; let offset = self.current_state.offset.to_vector().cast() * self.scale_factor; let font = fonts::match_font(&font_request, self.scale_factor); match font { fonts::Font::PixelFont(pf) => { let layout = fonts::text_layout_for_font(&pf, &font_request, self.scale_factor); let (horizontal_alignment, vertical_alignment) = text.alignment(); let paragraph = TextParagraphLayout { string: &string, layout, max_width: max_size.width_length(), max_height: max_size.height_length(), horizontal_alignment, vertical_alignment, wrap: text.wrap(), overflow: text.overflow(), single_line: false, }; self.draw_text_paragraph(¶graph, physical_clip, offset, color, None); } #[cfg(feature = "software-renderer-systemfonts")] fonts::Font::VectorFont(vf) => { let layout = fonts::text_layout_for_font(&vf, &font_request, self.scale_factor); let (horizontal_alignment, vertical_alignment) = text.alignment(); let paragraph = TextParagraphLayout { string: &string, layout, max_width: max_size.width_length(), max_height: max_size.height_length(), horizontal_alignment, vertical_alignment, wrap: text.wrap(), overflow: text.overflow(), single_line: false, }; self.draw_text_paragraph(¶graph, physical_clip, offset, color, None); } } } fn draw_text_input( &mut self, text_input: Pin<&crate::items::TextInput>, self_rc: &ItemRc, size: LogicalSize, ) { let geom = LogicalRect::from(size); if !self.should_draw(&geom) { return; } let font_request = text_input.font_request(self_rc); let max_size = (geom.size.cast() * self.scale_factor).cast(); // Clip glyphs not only against the global clip but also against the Text's geometry to avoid drawing outside // of its boundaries (that breaks partial rendering and the cast to usize for the item relative coordinate below). // FIXME: we should allow drawing outside of the Text element's boundaries. let physical_clip = if let Some(logical_clip) = self.current_state.clip.intersection(&geom) { logical_clip.cast() * self.scale_factor } else { return; // This should have been caught earlier already }; let offset = self.current_state.offset.to_vector().cast() * self.scale_factor; let font = fonts::match_font(&font_request, self.scale_factor); let text_visual_representation = text_input.visual_representation(None); let color = self.alpha_color(text_visual_representation.text_color.color()); let selection = (!text_visual_representation.selection_range.is_empty()).then_some(SelectionInfo { selection_background: self.alpha_color(text_input.selection_background_color()), selection_color: self.alpha_color(text_input.selection_foreground_color()), selection: text_visual_representation.selection_range.clone(), }); let cursor_pos_and_height = match font { fonts::Font::PixelFont(pf) => { let paragraph = TextParagraphLayout { string: &text_visual_representation.text, layout: fonts::text_layout_for_font(&pf, &font_request, self.scale_factor), max_width: max_size.width_length(), max_height: max_size.height_length(), horizontal_alignment: text_input.horizontal_alignment(), vertical_alignment: text_input.vertical_alignment(), wrap: text_input.wrap(), overflow: TextOverflow::Clip, single_line: text_input.single_line(), }; self.draw_text_paragraph(¶graph, physical_clip, offset, color, selection); text_visual_representation.cursor_position.map(|cursor_offset| { (paragraph.cursor_pos_for_byte_offset(cursor_offset), pf.height()) }) } #[cfg(feature = "software-renderer-systemfonts")] fonts::Font::VectorFont(vf) => { let paragraph = TextParagraphLayout { string: &text_visual_representation.text, layout: fonts::text_layout_for_font(&vf, &font_request, self.scale_factor), max_width: max_size.width_length(), max_height: max_size.height_length(), horizontal_alignment: text_input.horizontal_alignment(), vertical_alignment: text_input.vertical_alignment(), wrap: text_input.wrap(), overflow: TextOverflow::Clip, single_line: text_input.single_line(), }; self.draw_text_paragraph(¶graph, physical_clip, offset, color, selection); text_visual_representation.cursor_position.map(|cursor_offset| { (paragraph.cursor_pos_for_byte_offset(cursor_offset), vf.height()) }) } }; if let Some(((cursor_x, cursor_y), cursor_height)) = cursor_pos_and_height { let cursor_rect = PhysicalRect::new( PhysicalPoint::from_lengths(cursor_x, cursor_y), PhysicalSize::from_lengths( (text_input.text_cursor_width().cast() * self.scale_factor).cast(), cursor_height, ), ); if let Some(clipped_src) = cursor_rect.intersection(&physical_clip.cast()) { let geometry = clipped_src.translate(offset.cast()).transformed(self.rotation); #[allow(unused_mut)] let mut cursor_color = text_visual_representation.cursor_color; #[cfg(all(feature = "std", target_os = "macos"))] { // On macOs, the cursor color is different than other platform. Use a hack to pass the screenshot test. static IS_SCREENSHOT_TEST: std::sync::OnceLock = std::sync::OnceLock::new(); if *IS_SCREENSHOT_TEST.get_or_init(|| { std::env::var_os("CARGO_PKG_NAME").unwrap_or_default() == "test-driver-screenshots" }) { cursor_color = color; } } let args = target_pixel_buffer::DrawRectangleArgs::from_rect( geometry.cast(), self.alpha_color(cursor_color).into(), ); self.processor.process_rectangle(&args, geometry); } } } #[cfg(feature = "std")] fn draw_path(&mut self, _path: Pin<&crate::items::Path>, _: &ItemRc, _size: LogicalSize) { // TODO } fn draw_box_shadow( &mut self, _box_shadow: Pin<&crate::items::BoxShadow>, _: &ItemRc, _size: LogicalSize, ) { // TODO } fn combine_clip( &mut self, other: LogicalRect, _radius: LogicalBorderRadius, _border_width: LogicalLength, ) -> bool { match self.current_state.clip.intersection(&other) { Some(r) => { self.current_state.clip = r; true } None => { self.current_state.clip = LogicalRect::default(); false } } // TODO: handle radius and border } fn get_current_clip(&self) -> LogicalRect { self.current_state.clip } fn translate(&mut self, distance: LogicalVector) { self.current_state.offset += distance; self.current_state.clip = self.current_state.clip.translate(-distance) } fn translation(&self) -> LogicalVector { self.current_state.offset.to_vector() } fn rotate(&mut self, _angle_in_degrees: f32) { // TODO (#6068) } 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: &ItemRc, update_fn: &dyn Fn(&mut dyn FnMut(u32, u32, &[u8])), ) { // FIXME: actually cache the pixmap update_fn(&mut |width, height, data| { let img = SharedImageBuffer::RGBA8Premultiplied(SharedPixelBuffer::clone_from_slice( data, width, height, )); let physical_clip = (self.current_state.clip.cast() * self.scale_factor).cast(); let source_rect = euclid::rect(0, 0, width as _, height as _); if let Some(clipped_src) = source_rect.intersection(&physical_clip) { let geometry = clipped_src .translate( (self.current_state.offset.cast() * self.scale_factor).to_vector().cast(), ) .round_in(); let t = target_pixel_buffer::DrawTextureArgs { data: target_pixel_buffer::TextureDataContainer::Shared { buffer: SharedBufferData::SharedImage(img), source_rect, }, colorize: None, alpha: (self.current_state.alpha * 255.) as u8, dst_x: self.current_state.offset.x as _, dst_y: self.current_state.offset.y as _, dst_width: width as _, dst_height: height as _, rotation: self.rotation.orientation, tiling: None, }; self.processor .process_target_texture(&t, geometry.cast().transformed(self.rotation)); } }); } fn draw_string(&mut self, string: &str, color: Color) { let font_request = Default::default(); let font = fonts::match_font(&font_request, self.scale_factor); let clip = self.current_state.clip.cast() * self.scale_factor; match font { fonts::Font::PixelFont(pf) => { let layout = fonts::text_layout_for_font(&pf, &font_request, self.scale_factor); let paragraph = TextParagraphLayout { string, layout, max_width: clip.width_length().cast(), max_height: clip.height_length().cast(), horizontal_alignment: Default::default(), vertical_alignment: Default::default(), wrap: Default::default(), overflow: Default::default(), single_line: false, }; self.draw_text_paragraph(¶graph, clip, Default::default(), color, None); } #[cfg(feature = "software-renderer-systemfonts")] fonts::Font::VectorFont(vf) => { let layout = fonts::text_layout_for_font(&vf, &font_request, self.scale_factor); let paragraph = TextParagraphLayout { string, layout, max_width: clip.width_length().cast(), max_height: clip.height_length().cast(), horizontal_alignment: Default::default(), vertical_alignment: Default::default(), wrap: Default::default(), overflow: Default::default(), single_line: false, }; self.draw_text_paragraph(¶graph, clip, Default::default(), color, None); } } } fn draw_image_direct(&mut self, _image: crate::graphics::Image) { todo!() } fn window(&self) -> &crate::window::WindowInner { self.window } fn as_any(&mut self) -> Option<&mut dyn core::any::Any> { None } } impl crate::item_rendering::ItemRendererFeatures for SceneBuilder<'_, T> { const SUPPORTS_TRANSFORMATIONS: bool = false; }