Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
8 changes: 5 additions & 3 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,7 +51,7 @@ unsafe-single-threaded = []
unicode = ["unicode-script", "unicode-linebreak"]

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

image-decoders = ["dep:image", "dep:clru"]
image-default-formats = ["image?/default-formats"]
Expand Down Expand Up @@ -107,6 +108,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
136 changes: 134 additions & 2 deletions internal/core/software_renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ mod draw_functions;
mod fixed;
mod fonts;
mod minimal_software_window;
mod path;
mod scene;

use self::fonts::GlyphRenderer;
Expand Down Expand Up @@ -1301,6 +1302,21 @@ 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);
fn process_filled_path(
&mut self,
path_geometry: PhysicalRect,
clip_geometry: PhysicalRect,
commands: alloc::vec::Vec<path::Command>,
color: PremultipliedRgbaColor,
);
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 +1697,34 @@ impl<B: target_pixel_buffer::TargetPixelBuffer> ProcessScene for RenderToBuffer<
);
});
}

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);
}

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 +1858,29 @@ impl ProcessScene for PrepareScene {
});
}
}

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)
}

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 @@ -2679,8 +2746,73 @@ 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
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
169 changes: 169 additions & 0 deletions internal/core/software_renderer/path.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
// 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(any(feature = "std"))]
use crate::graphics::PathDataIterator;
use alloc::vec::Vec;
pub use zeno::Command;
use zeno::{Fill, Mask, Point, Stroke};

/// Convert Slint's PathDataIterator to zeno's Command format
#[cfg(any(feature = "std"))]
pub fn convert_path_data_to_zeno(path_data: PathDataIterator) -> Vec<Command> {
use lyon_path::Event;
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