Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions internal/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ categories = ["gui", "development-tools", "no-std"]
path = "lib.rs"

[features]
ffi = ["dep:static_assertions"] # Expose C ABI
libm = ["num-traits/libm", "euclid/libm"]
ffi = ["dep:static_assertions"] # Expose C ABI
libm = ["num-traits/libm", "euclid/libm", "zeno?/libm"]
# Allow the viewer to query at runtime information about item types
rtti = []
# Use the standard library
Expand All @@ -37,6 +37,7 @@ std = [
"image-decoders",
"svg",
"raw-window-handle-06?/std",
"zeno?/std",
"chrono/std",
"chrono/wasmbind",
"chrono/clock",
Expand All @@ -50,6 +51,7 @@ unsafe-single-threaded = []
unicode = ["unicode-script", "unicode-linebreak"]

software-renderer-systemfonts = ["shared-fontique", "skrifa", "fontdue", "software-renderer", "shared-parley"]
software-renderer-path = ["dep:zeno"]
software-renderer = ["bytemuck"]

image-decoders = ["dep:image", "dep:clru"]
Expand Down Expand Up @@ -107,6 +109,7 @@ unicode-linebreak = { version = "0.1.5", optional = true }
unicode-script = { version = "0.5.7", optional = true }
integer-sqrt = { version = "0.1.5" }
bytemuck = { workspace = true, optional = true, features = ["derive"] }
zeno = { version = "0.3.3", optional = true, default-features = false, features = ["eval"] }
sys-locale = { version = "0.3.2", optional = true }
parley = { version = "0.6.0", optional = true }
pulldown-cmark = { version = "0.13.0", optional = true }
Expand Down
155 changes: 152 additions & 3 deletions internal/core/software_renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ mod draw_functions;
mod fixed;
mod fonts;
mod minimal_software_window;
#[cfg(feature = "software-renderer-path")]
mod path;
mod scene;

use self::fonts::GlyphRenderer;
Expand Down Expand Up @@ -1301,6 +1303,23 @@ trait ProcessScene {
fn process_linear_gradient(&mut self, geometry: PhysicalRect, gradient: LinearGradientCommand);
fn process_radial_gradient(&mut self, geometry: PhysicalRect, gradient: RadialGradientCommand);
fn process_conic_gradient(&mut self, geometry: PhysicalRect, gradient: ConicGradientCommand);
#[cfg(feature = "software-renderer-path")]
fn process_filled_path(
&mut self,
path_geometry: PhysicalRect,
clip_geometry: PhysicalRect,
commands: alloc::vec::Vec<path::Command>,
color: PremultipliedRgbaColor,
);
#[cfg(feature = "software-renderer-path")]
fn process_stroked_path(
&mut self,
path_geometry: PhysicalRect,
clip_geometry: PhysicalRect,
commands: alloc::vec::Vec<path::Command>,
color: PremultipliedRgbaColor,
stroke_width: f32,
);
}

fn process_rectangle_impl(
Expand Down Expand Up @@ -1681,6 +1700,36 @@ impl<B: target_pixel_buffer::TargetPixelBuffer> ProcessScene for RenderToBuffer<
);
});
}

#[cfg(feature = "software-renderer-path")]
fn process_filled_path(
&mut self,
path_geometry: PhysicalRect,
clip_geometry: PhysicalRect,
commands: alloc::vec::Vec<path::Command>,
color: PremultipliedRgbaColor,
) {
path::render_filled_path(&commands, &path_geometry, &clip_geometry, color, self.buffer);
}

#[cfg(feature = "software-renderer-path")]
fn process_stroked_path(
&mut self,
path_geometry: PhysicalRect,
clip_geometry: PhysicalRect,
commands: alloc::vec::Vec<path::Command>,
color: PremultipliedRgbaColor,
stroke_width: f32,
) {
path::render_stroked_path(
&commands,
&path_geometry,
&clip_geometry,
color,
stroke_width,
self.buffer,
);
}
}

#[derive(Default)]
Expand Down Expand Up @@ -1814,6 +1863,31 @@ impl ProcessScene for PrepareScene {
});
}
}

#[cfg(feature = "software-renderer-path")]
fn process_filled_path(
&mut self,
_path_geometry: PhysicalRect,
_clip_geometry: PhysicalRect,
_commands: alloc::vec::Vec<path::Command>,
_color: PremultipliedRgbaColor,
) {
// Path rendering is not supported in line-by-line mode (PrepareScene/render_by_line)
// Only works with buffer-based rendering (RenderToBuffer)
}

#[cfg(feature = "software-renderer-path")]
fn process_stroked_path(
&mut self,
_path_geometry: PhysicalRect,
_clip_geometry: PhysicalRect,
_commands: alloc::vec::Vec<path::Command>,
_color: PremultipliedRgbaColor,
_stroke_width: f32,
) {
// Path rendering is not supported in line-by-line mode (PrepareScene/render_by_line)
// Only works with buffer-based rendering (RenderToBuffer)
}
}

struct SceneBuilder<'a, T> {
Expand Down Expand Up @@ -2678,9 +2752,84 @@ impl<T: ProcessScene> crate::item_rendering::ItemRenderer for SceneBuilder<'_, T
}
}

#[cfg(feature = "std")]
fn draw_path(&mut self, _path: Pin<&crate::items::Path>, _: &ItemRc, _size: LogicalSize) {
// TODO
#[cfg(all(feature = "std", not(feature = "software-renderer-path")))]
fn draw_path(
&mut self,
_path: Pin<&crate::items::Path>,
_self_rc: &ItemRc,
_size: LogicalSize,
) {
// Path rendering is disabled without the software-renderer-path feature
}

#[cfg(all(feature = "std", feature = "software-renderer-path"))]
fn draw_path(&mut self, path: Pin<&crate::items::Path>, self_rc: &ItemRc, size: LogicalSize) {
let geom = LogicalRect::from(size);
if !self.should_draw(&geom) {
return;
}

// Get the fitted path events from the Path item
let Some((offset, path_iterator)) = path.fitted_path_events(self_rc) else {
return;
};

// Convert to zeno commands
let zeno_commands = path::convert_path_data_to_zeno(path_iterator);

// Calculate the physical geometry
let physical_geom = (geom.translate(self.current_state.offset.to_vector()).cast()
* self.scale_factor)
.round()
.cast()
.transformed(self.rotation);

let physical_clip =
(self.current_state.clip.translate(self.current_state.offset.to_vector()).cast()
* self.scale_factor)
.round()
.cast::<i16>()
.transformed(self.rotation);

// Apply the offset from fitted path
let physical_offset = (offset.cast::<f32>() * self.scale_factor).cast::<i16>();
let adjusted_geom = physical_geom.translate(physical_offset);

// Clip the geometry - early return if nothing to draw
let Some(clipped_geom) = adjusted_geom.intersection(&physical_clip) else {
return;
};

// Draw fill if specified
let fill_brush = path.fill();
if !fill_brush.is_transparent() {
let fill_color = self.alpha_color(fill_brush.color());
if fill_color.alpha() > 0 {
self.processor.process_filled_path(
adjusted_geom,
clipped_geom,
zeno_commands.clone(),
fill_color.into(),
);
}
}

// Draw stroke if specified
let stroke_brush = path.stroke();
let stroke_width = path.stroke_width();
if !stroke_brush.is_transparent() && stroke_width.get() > 0.0 {
let stroke_color = self.alpha_color(stroke_brush.color());
if stroke_color.alpha() > 0 {
let physical_stroke_width = (stroke_width.cast() * self.scale_factor).get();
self.processor.process_stroked_path(
adjusted_geom,
clipped_geom,
zeno_commands,
stroke_color.into(),
physical_stroke_width,
);
}
}
}

fn draw_box_shadow(
Expand Down
171 changes: 171 additions & 0 deletions internal/core/software_renderer/path.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
// Copyright © SixtyFPS GmbH <[email protected]>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0

//! Path rendering support for the software renderer using zeno

use super::draw_functions::{PremultipliedRgbaColor, TargetPixel};
use super::PhysicalRect;
#[cfg(feature = "std")]
use crate::graphics::PathDataIterator;
use alloc::vec::Vec;
use zeno::{Fill, Mask, Stroke};

pub use zeno::Command;

/// Convert Slint's PathDataIterator to zeno's Command format
#[cfg(feature = "std")]
pub fn convert_path_data_to_zeno(path_data: PathDataIterator) -> Vec<Command> {
use lyon_path::Event;
use zeno::Point;
let mut commands = Vec::new();

for event in path_data.iter() {
match event {
Event::Begin { at } => {
commands.push(Command::MoveTo(Point::new(at.x, at.y)));
}
Event::Line { to, .. } => {
commands.push(Command::LineTo(Point::new(to.x, to.y)));
}
Event::Quadratic { ctrl, to, .. } => {
commands.push(Command::QuadTo(Point::new(ctrl.x, ctrl.y), Point::new(to.x, to.y)));
}
Event::Cubic { ctrl1, ctrl2, to, .. } => {
commands.push(Command::CurveTo(
Point::new(ctrl1.x, ctrl1.y),
Point::new(ctrl2.x, ctrl2.y),
Point::new(to.x, to.y),
));
}
Event::End { close, .. } => {
if close {
commands.push(Command::Close);
}
}
}
}

commands
}

/// Common rendering logic for both filled and stroked paths
fn render_path_with_style<T: TargetPixel>(
commands: &[Command],
path_geometry: &PhysicalRect,
clip_geometry: &PhysicalRect,
color: PremultipliedRgbaColor,
style: zeno::Style,
buffer: &mut impl crate::software_renderer::target_pixel_buffer::TargetPixelBuffer<TargetPixel = T>,
) {
// The mask needs to be rendered at the full path size
let path_width = path_geometry.size.width as usize;
let path_height = path_geometry.size.height as usize;

if path_width == 0 || path_height == 0 {
return;
}

// Create a buffer for the mask output
let mut mask_buffer = Vec::with_capacity(path_width * path_height);
mask_buffer.resize(path_width * path_height, 0u8);

// Render the full path into the mask
Mask::new(commands)
.size(path_width as u32, path_height as u32)
.style(style)
.render_into(&mut mask_buffer, None);

// Calculate the intersection region - only apply within clipped area
// clip_geometry is relative to screen, path_geometry is also relative to screen
let clip_x_start = clip_geometry.origin.x.max(0) as usize;
let clip_y_start = clip_geometry.origin.y.max(0) as usize;
let clip_x_end = (clip_geometry.max_x().max(0) as usize).min(buffer.line_slice(0).len());
let clip_y_end = (clip_geometry.max_y().max(0) as usize).min(buffer.num_lines());

let path_x_start = path_geometry.origin.x as isize;
let path_y_start = path_geometry.origin.y as isize;

// Apply the mask only within the clipped region
for screen_y in clip_y_start..clip_y_end {
let line = buffer.line_slice(screen_y);

// Calculate the y coordinate in the mask buffer
let mask_y = screen_y as isize - path_y_start;
if mask_y < 0 || mask_y >= path_height as isize {
continue;
}

for screen_x in clip_x_start..clip_x_end {
// Calculate the x coordinate in the mask buffer
let mask_x = screen_x as isize - path_x_start;
if mask_x < 0 || mask_x >= path_width as isize {
continue;
}

let mask_idx = (mask_y as usize) * path_width + (mask_x as usize);
let coverage = mask_buffer[mask_idx];

if coverage > 0 {
// Scale all color components by coverage to maintain premultiplication
let coverage_factor = coverage as u16;
let alpha_color = PremultipliedRgbaColor {
red: ((color.red as u16 * coverage_factor) / 255) as u8,
green: ((color.green as u16 * coverage_factor) / 255) as u8,
blue: ((color.blue as u16 * coverage_factor) / 255) as u8,
alpha: ((color.alpha as u16 * coverage_factor) / 255) as u8,
};
T::blend(&mut line[screen_x], alpha_color);
}
}
}
}

/// Render a filled path
///
/// * `commands` - The path commands to render
/// * `path_geometry` - The full bounding box of the path in screen coordinates
/// * `clip_geometry` - The clipped region where the path should be rendered (intersection of path and clip)
/// * `color` - The color to render the path
/// * `buffer` - The target pixel buffer
pub fn render_filled_path<T: TargetPixel>(
commands: &[Command],
path_geometry: &PhysicalRect,
clip_geometry: &PhysicalRect,
color: PremultipliedRgbaColor,
buffer: &mut impl crate::software_renderer::target_pixel_buffer::TargetPixelBuffer<TargetPixel = T>,
) {
render_path_with_style(
commands,
path_geometry,
clip_geometry,
color,
zeno::Style::Fill(Fill::NonZero),
buffer,
);
}

/// Render a stroked path
///
/// * `commands` - The path commands to render
/// * `path_geometry` - The full bounding box of the path in screen coordinates
/// * `clip_geometry` - The clipped region where the path should be rendered (intersection of path and clip)
/// * `color` - The color to render the path
/// * `stroke_width` - The width of the stroke
/// * `buffer` - The target pixel buffer
pub fn render_stroked_path<T: TargetPixel>(
commands: &[Command],
path_geometry: &PhysicalRect,
clip_geometry: &PhysicalRect,
color: PremultipliedRgbaColor,
stroke_width: f32,
buffer: &mut impl crate::software_renderer::target_pixel_buffer::TargetPixelBuffer<TargetPixel = T>,
) {
render_path_with_style(
commands,
path_geometry,
clip_geometry,
color,
zeno::Style::Stroke(Stroke::new(stroke_width)),
buffer,
);
}
Loading