Software renderer: use parley (#9656)

* Store font index and blob instead of full fontique::QueryFont

* fontdue changes

* VectorFont takes blob and index

* Start implementing sharedparley::GlyphRenderer for SceneBuilder

* Change brush to be a color as the sw renderer can't do fancy gradients
anyway

* Implement trait fully

* Use trait instead if correct features are set

* Remove gl_x

* Slight fixes for the parley update

* [autofix.ci] apply automated fixes

* Add a RenderableVectorGlyph type in order to compile type enforce vector
alpha map and sdf stuff

* Add shared-parley to software-renderer-systemfonts feature

* Check font type before branching into legacy or parley codepaths

* Fix fill_rectangle offset

* Set alpha colors, physical clip

* Adjust font_size in swrenderer

* Apply rotation to selection rectangle

Switch order

[autofix.ci] apply automated fixes
Add missing mut

* Use sharedparley for text_size and font_metrics

* Put RenderableVectorGlyph behind feature flag

* [autofix.ci] apply automated fixes

* Remove unused font functions

* Allow rendering without parley

* Add missing cfg flags

* Use env variable instead

* Use font.index in glyph cache key

* Handle font matching and fallbacks differently

* Update references

* Put the parley disabled check behind a cfg flag

* Update references again

* Fix fill_rectangle rounding

* [autofix.ci] apply automated fixes

* Add bounds.xmin to target_rect

* Update references

* Update tests after bounds changes

* Round target_rect to fix rotate180 screenshot tests

* Update references yet again

* Get rid of rounding on target rect

* Use casts to properly truncate numbers. Hopefully tests now pass.

* Update references for hopefully final time

* Again

* Update references yet again

* Revert match_font, screenshot references

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Ashley 2025-10-14 15:19:49 +02:00 committed by GitHub
parent 13c7db5330
commit b31aba96ce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 626 additions and 264 deletions

View file

@ -48,7 +48,7 @@ unsafe-single-threaded = []
unicode = ["unicode-script", "unicode-linebreak"]
software-renderer-systemfonts = ["shared-fontique", "rustybuzz", "fontdue", "software-renderer"]
software-renderer-systemfonts = ["shared-fontique", "rustybuzz", "fontdue", "software-renderer", "shared-parley"]
software-renderer = ["bytemuck"]
image-decoders = ["dep:image", "dep:clru"]

View file

@ -717,7 +717,35 @@ impl RendererSealed for SoftwareRenderer {
scale_factor: ScaleFactor,
text_wrap: TextWrap,
) -> LogicalSize {
fonts::text_size(font_request, text, max_width, scale_factor, text_wrap)
let font = fonts::match_font(&font_request, scale_factor);
match (font, parley_disabled()) {
#[cfg(feature = "software-renderer-systemfonts")]
(fonts::Font::VectorFont(_), false) => {
sharedparley::text_size(font_request, text, max_width, scale_factor, text_wrap)
}
#[cfg(feature = "software-renderer-systemfonts")]
(fonts::Font::VectorFont(vf), true) => {
let layout = fonts::text_layout_for_font(&vf, &font_request, scale_factor);
let (longest_line_width, height) = layout.text_size(
text,
max_width.map(|max_width| (max_width.cast() * scale_factor).cast()),
text_wrap,
);
(PhysicalSize::from_lengths(longest_line_width, height).cast() / scale_factor)
.cast()
}
(fonts::Font::PixelFont(pf), _) => {
let layout = fonts::text_layout_for_font(&pf, &font_request, scale_factor);
let (longest_line_width, height) = layout.text_size(
text,
max_width.map(|max_width| (max_width.cast() * scale_factor).cast()),
text_wrap,
);
(PhysicalSize::from_lengths(longest_line_width, height).cast() / scale_factor)
.cast()
}
}
}
fn font_metrics(
@ -725,7 +753,39 @@ impl RendererSealed for SoftwareRenderer {
font_request: crate::graphics::FontRequest,
scale_factor: ScaleFactor,
) -> crate::items::FontMetrics {
fonts::font_metrics(font_request, scale_factor)
let font = fonts::match_font(&font_request, scale_factor);
match (font, parley_disabled()) {
#[cfg(feature = "software-renderer-systemfonts")]
(fonts::Font::VectorFont(_), false) => sharedparley::font_metrics(font_request),
#[cfg(feature = "software-renderer-systemfonts")]
(fonts::Font::VectorFont(font), true) => {
let ascent: LogicalLength = (font.ascent().cast() / scale_factor).cast();
let descent: LogicalLength = (font.descent().cast() / scale_factor).cast();
let x_height: LogicalLength = (font.x_height().cast() / scale_factor).cast();
let cap_height: LogicalLength = (font.cap_height().cast() / scale_factor).cast();
crate::items::FontMetrics {
ascent: ascent.get() as _,
descent: descent.get() as _,
x_height: x_height.get() as _,
cap_height: cap_height.get() as _,
}
}
(fonts::Font::PixelFont(font), _) => {
let ascent: LogicalLength = (font.ascent().cast() / scale_factor).cast();
let descent: LogicalLength = (font.descent().cast() / scale_factor).cast();
let x_height: LogicalLength = (font.x_height().cast() / scale_factor).cast();
let cap_height: LogicalLength = (font.cap_height().cast() / scale_factor).cast();
crate::items::FontMetrics {
ascent: ascent.get() as _,
descent: descent.get() as _,
x_height: x_height.get() as _,
cap_height: cap_height.get() as _,
}
}
}
}
fn text_input_byte_offset_for_position(
@ -735,20 +795,30 @@ impl RendererSealed for SoftwareRenderer {
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();
match (font, parley_disabled()) {
#[cfg(feature = "software-renderer-systemfonts")]
(fonts::Font::VectorFont(_), false) => {
sharedparley::text_input_byte_offset_for_position(
text_input,
pos,
font_request,
scale_factor,
)
}
#[cfg(feature = "software-renderer-systemfonts")]
(fonts::Font::VectorFont(vf), true) => {
let visual_representation = text_input.visual_representation(None);
let pos = (pos.cast() * scale_factor)
.clamp(euclid::point2(0., 0.), euclid::point2(i16::MAX, i16::MAX).cast())
.cast();
let width = (text_input.width().cast() * scale_factor).cast();
let height = (text_input.height().cast() * scale_factor).cast();
match font {
fonts::Font::PixelFont(pf) => {
let layout = fonts::text_layout_for_font(&pf, &font_request, scale_factor);
let pos = (pos.cast() * scale_factor)
.clamp(euclid::point2(0., 0.), euclid::point2(i16::MAX, i16::MAX).cast())
.cast();
let layout = fonts::text_layout_for_font(&vf, &font_request, scale_factor);
let paragraph = TextParagraphLayout {
string: &visual_representation.text,
@ -766,9 +836,17 @@ impl RendererSealed for SoftwareRenderer {
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);
(fonts::Font::PixelFont(pf), _) => {
let visual_representation = text_input.visual_representation(None);
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();
let layout = fonts::text_layout_for_font(&pf, &font_request, scale_factor);
let paragraph = TextParagraphLayout {
string: &visual_representation.text,
@ -796,33 +874,25 @@ impl RendererSealed for SoftwareRenderer {
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())
match (font, parley_disabled()) {
#[cfg(feature = "software-renderer-systemfonts")]
(fonts::Font::VectorFont(_), false) => {
sharedparley::text_input_cursor_rect_for_byte_offset(
text_input,
byte_offset,
font_request,
scale_factor,
)
}
#[cfg(feature = "software-renderer-systemfonts")]
fonts::Font::VectorFont(vf) => {
(fonts::Font::VectorFont(vf), true) => {
let visual_representation = text_input.visual_representation(None);
let width = (text_input.width().cast() * scale_factor).cast();
let height = (text_input.height().cast() * scale_factor).cast();
let layout = fonts::text_layout_for_font(&vf, &font_request, scale_factor);
let paragraph = TextParagraphLayout {
@ -837,20 +907,55 @@ impl RendererSealed for SoftwareRenderer {
single_line: false,
};
(paragraph.cursor_pos_for_byte_offset(byte_offset), vf.height())
}
};
let cursor_position = paragraph.cursor_pos_for_byte_offset(byte_offset);
let cursor_height = 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()
(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()
}
(fonts::Font::PixelFont(pf), _) => {
let visual_representation = text_input.visual_representation(None);
let width = (text_input.width().cast() * scale_factor).cast();
let height = (text_input.height().cast() * scale_factor).cast();
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,
};
let cursor_position = paragraph.cursor_pos_for_byte_offset(byte_offset);
let cursor_height = pf.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(
@ -937,6 +1042,15 @@ impl RendererSealed for SoftwareRenderer {
}
}
fn parley_disabled() -> bool {
#[cfg(feature = "software-renderer-systemfonts")]
{
std::env::var("SLINT_SOFTWARE_RENDERER_PARLEY_DISABLED").is_ok()
}
#[cfg(not(feature = "software-renderer-systemfonts"))]
false
}
fn render_window_frame_by_line(
window: &WindowInner,
background: Brush,
@ -2272,36 +2386,41 @@ impl<T: ProcessScene> crate::item_rendering::ItemRenderer for SceneBuilder<'_, T
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);
match (font, parley_disabled()) {
#[cfg(feature = "software-renderer-systemfonts")]
(fonts::Font::VectorFont(_), false) => {
sharedparley::draw_text(self, text, Some(text.font_request(self_rc)), size);
}
#[cfg(feature = "software-renderer-systemfonts")]
(fonts::Font::VectorFont(vf), true) => {
let string = text.text();
if string.trim().is_empty() {
return;
}
let geom = LogicalRect::from(size);
if !self.should_draw(&geom) {
return;
}
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 layout = fonts::text_layout_for_font(&vf, &font_request, self.scale_factor);
let (horizontal_alignment, vertical_alignment) = text.alignment();
let paragraph = TextParagraphLayout {
@ -2318,9 +2437,31 @@ impl<T: ProcessScene> crate::item_rendering::ItemRenderer for SceneBuilder<'_, T
self.draw_text_paragraph(&paragraph, 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);
(fonts::Font::PixelFont(pf), _) => {
let string = text.text();
if string.trim().is_empty() {
return;
}
let geom = LogicalRect::from(size);
if !self.should_draw(&geom) {
return;
}
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 layout = fonts::text_layout_for_font(&pf, &font_request, self.scale_factor);
let (horizontal_alignment, vertical_alignment) = text.alignment();
let paragraph = TextParagraphLayout {
@ -2346,59 +2487,52 @@ impl<T: ProcessScene> crate::item_rendering::ItemRenderer for SceneBuilder<'_, T
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(&paragraph, physical_clip, offset, color, selection);
text_visual_representation.cursor_position.map(|cursor_offset| {
(paragraph.cursor_pos_for_byte_offset(cursor_offset), pf.height())
})
match (font, parley_disabled()) {
#[cfg(feature = "software-renderer-systemfonts")]
(fonts::Font::VectorFont(_), false) => {
sharedparley::draw_text_input(
self,
text_input,
Some(text_input.font_request(self_rc)),
size,
None,
);
}
#[cfg(feature = "software-renderer-systemfonts")]
fonts::Font::VectorFont(vf) => {
(fonts::Font::VectorFont(vf), true) => {
let geom = LogicalRect::from(size);
if !self.should_draw(&geom) {
return;
}
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 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 paragraph = TextParagraphLayout {
string: &text_visual_representation.text,
layout: fonts::text_layout_for_font(&vf, &font_request, self.scale_factor),
@ -2413,42 +2547,128 @@ impl<T: ProcessScene> crate::item_rendering::ItemRenderer for SceneBuilder<'_, T
self.draw_text_paragraph(&paragraph, physical_clip, offset, color, selection);
text_visual_representation.cursor_position.map(|cursor_offset| {
(paragraph.cursor_pos_for_byte_offset(cursor_offset), vf.height())
})
}
};
let cursor_pos_and_height =
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(((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<bool> =
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;
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<bool> =
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);
}
}
let args = target_pixel_buffer::DrawRectangleArgs::from_rect(
geometry.cast(),
self.alpha_color(cursor_color).into(),
}
(fonts::Font::PixelFont(pf), _) => {
let geom = LogicalRect::from(size);
if !self.should_draw(&geom) {
return;
}
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 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(),
},
);
self.processor.process_rectangle(&args, geometry);
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(&paragraph, physical_clip, offset, color, selection);
let cursor_pos_and_height =
text_visual_representation.cursor_position.map(|cursor_offset| {
(paragraph.cursor_pos_for_byte_offset(cursor_offset), pf.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<bool> =
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);
}
}
}
}
}
@ -2566,9 +2786,19 @@ impl<T: ProcessScene> crate::item_rendering::ItemRenderer for SceneBuilder<'_, T
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);
match (font, parley_disabled()) {
#[cfg(feature = "software-renderer-systemfonts")]
(fonts::Font::VectorFont(_), false) => {
sharedparley::draw_text(
self,
std::pin::pin!((crate::SharedString::from(string), Brush::from(color))),
None,
LogicalSize::new(1., 1.), // Non-zero size to avoid an early return
);
}
#[cfg(feature = "software-renderer-systemfonts")]
(fonts::Font::VectorFont(vf), true) => {
let layout = fonts::text_layout_for_font(&vf, &font_request, self.scale_factor);
let paragraph = TextParagraphLayout {
string,
@ -2584,9 +2814,8 @@ impl<T: ProcessScene> crate::item_rendering::ItemRenderer for SceneBuilder<'_, T
self.draw_text_paragraph(&paragraph, 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);
(fonts::Font::PixelFont(pf), _) => {
let layout = fonts::text_layout_for_font(&pf, &font_request, self.scale_factor);
let paragraph = TextParagraphLayout {
string,
@ -2621,3 +2850,137 @@ impl<T: ProcessScene> crate::item_rendering::ItemRenderer for SceneBuilder<'_, T
impl<T: ProcessScene> crate::item_rendering::ItemRendererFeatures for SceneBuilder<'_, T> {
const SUPPORTS_TRANSFORMATIONS: bool = false;
}
#[cfg(feature = "software-renderer-systemfonts")]
use crate::textlayout::sharedparley;
#[cfg(feature = "software-renderer-systemfonts")]
impl<T: ProcessScene> sharedparley::GlyphRenderer for SceneBuilder<'_, T> {
type PlatformBrush = Color;
fn platform_brush_for_color(&mut self, color: &Color) -> Option<Self::PlatformBrush> {
Some(*color)
}
fn platform_text_fill_brush(
&mut self,
brush: Brush,
_size: LogicalSize,
) -> Option<Self::PlatformBrush> {
match brush {
Brush::SolidColor(color) => Some(color),
_ => None,
}
}
fn platform_text_stroke_brush(
&mut self,
brush: Brush,
_physical_stroke_width: f32,
_size: LogicalSize,
) -> Option<Self::PlatformBrush> {
match brush {
Brush::SolidColor(color) => Some(color),
_ => None,
}
}
fn fill_rectangle(&mut self, mut physical_rect: sharedparley::PhysicalRect, color: Color) {
if color.alpha() == 0 {
return;
}
let global_offset =
(self.current_state.offset.to_vector().cast() * self.scale_factor).cast();
physical_rect.origin += global_offset;
let physical_rect = physical_rect.cast().transformed(self.rotation);
let args = target_pixel_buffer::DrawRectangleArgs::from_rect(
physical_rect.cast(),
Brush::SolidColor(color),
);
self.processor.process_rectangle(&args, physical_rect);
}
fn draw_glyph_run(
&mut self,
font: &sharedparley::parley::FontData,
font_size: sharedparley::PhysicalLength,
color: Self::PlatformBrush,
y_offset: sharedparley::PhysicalLength,
glyphs_it: &mut dyn Iterator<Item = sharedparley::parley::layout::Glyph>,
) {
let fontdue_font = fonts::systemfonts::get_or_create_fontdue_font_from_blob_and_index(
&font.data, font.index,
);
let font = fonts::vectorfont::VectorFont::new_from_blob_and_index(
font.data.clone(),
font.index,
fontdue_font,
font_size.cast(),
);
let global_offset =
(self.current_state.offset.to_vector().cast() * self.scale_factor).cast();
for positioned_glyph in glyphs_it {
let Some(glyph) = std::num::NonZero::new(positioned_glyph.id as u16)
.and_then(|id| font.render_vector_glyph(id))
else {
continue;
};
let glyph_offset: euclid::Vector2D<i16, PhysicalPx> = euclid::Vector2D::from_lengths(
euclid::Length::new(positioned_glyph.x),
euclid::Length::new(positioned_glyph.y) + y_offset,
)
.cast();
let gl_y = PhysicalLength::new(glyph.y.truncate() as i16);
let target_rect: PhysicalRect = euclid::Rect::<f32, PhysicalPx>::new(
(PhysicalPoint::from_lengths(PhysicalLength::new(0), -gl_y - glyph.height)
+ global_offset
+ glyph_offset)
.cast()
+ euclid::vec2(glyph.bounds.xmin, 0.0),
glyph.size().cast(),
)
.cast()
.transformed(self.rotation);
let data = {
let source_rect = euclid::rect(0, 0, glyph.width.0, glyph.height.0);
target_pixel_buffer::TextureDataContainer::Shared {
buffer: SharedBufferData::AlphaMap {
data: glyph.alpha_map,
width: glyph.pixel_stride,
},
source_rect,
}
};
let color = self.alpha_color(color);
let physical_clip =
(self.current_state.clip.translate(self.current_state.offset.to_vector()).cast()
* self.scale_factor)
.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, physical_clip.cast());
}
}
}

View file

@ -7,9 +7,8 @@ use core::cell::RefCell;
use super::{Fixed, PhysicalLength, PhysicalSize};
use crate::graphics::{BitmapFont, FontRequest};
use crate::items::TextWrap;
use crate::lengths::{LogicalLength, LogicalSize, ScaleFactor};
use crate::textlayout::{FontMetrics, TextLayout};
use crate::lengths::{LogicalLength, ScaleFactor};
use crate::textlayout::TextLayout;
use crate::Coord;
crate::thread_local! {
@ -39,6 +38,26 @@ impl RenderableGlyph {
}
}
// Subset of `RenderableGlyph`, specfically for VectorFonts.
#[cfg(feature = "software-renderer-systemfonts")]
#[derive(Clone)]
pub struct RenderableVectorGlyph {
pub x: Fixed<i32, 8>,
pub y: Fixed<i32, 8>,
pub width: PhysicalLength,
pub height: PhysicalLength,
pub alpha_map: Rc<[u8]>,
pub pixel_stride: u16,
pub bounds: fontdue::OutlineBounds,
}
#[cfg(feature = "software-renderer-systemfonts")]
impl RenderableVectorGlyph {
pub fn size(&self) -> PhysicalSize {
PhysicalSize::from_lengths(self.width, self.height)
}
}
pub trait GlyphRenderer {
fn render_glyph(&self, glyph_id: core::num::NonZeroU16) -> Option<RenderableGlyph>;
/// The amount of pixel in the original image that correspond to one pixel in the rendered image
@ -182,53 +201,3 @@ where
pub fn register_bitmap_font(font_data: &'static BitmapFont) {
BITMAP_FONTS.with(|fonts| fonts.borrow_mut().push(font_data))
}
pub fn text_size(
font_request: FontRequest,
text: &str,
max_width: Option<LogicalLength>,
scale_factor: ScaleFactor,
text_wrap: TextWrap,
) -> LogicalSize {
let font = match_font(&font_request, scale_factor);
let (longest_line_width, height) = match font {
Font::PixelFont(pf) => {
let layout = text_layout_for_font(&pf, &font_request, scale_factor);
layout.text_size(
text,
max_width.map(|max_width| (max_width.cast() * scale_factor).cast()),
text_wrap,
)
}
#[cfg(feature = "software-renderer-systemfonts")]
Font::VectorFont(vf) => {
let layout = text_layout_for_font(&vf, &font_request, scale_factor);
layout.text_size(
text,
max_width.map(|max_width| (max_width.cast() * scale_factor).cast()),
text_wrap,
)
}
};
(PhysicalSize::from_lengths(longest_line_width, height).cast() / scale_factor).cast()
}
pub fn font_metrics(
font_request: FontRequest,
scale_factor: ScaleFactor,
) -> crate::items::FontMetrics {
let font = match_font(&font_request, scale_factor);
let ascent: LogicalLength = (font.ascent().cast() / scale_factor).cast();
let descent: LogicalLength = (font.descent().cast() / scale_factor).cast();
let x_height: LogicalLength = (font.x_height().cast() / scale_factor).cast();
let cap_height: LogicalLength = (font.cap_height().cast() / scale_factor).cast();
crate::items::FontMetrics {
ascent: ascent.get() as _,
descent: descent.get() as _,
x_height: x_height.get() as _,
cap_height: cap_height.get() as _,
}
}

View file

@ -14,19 +14,22 @@ use super::super::PhysicalLength;
use super::vectorfont::VectorFont;
crate::thread_local! {
static FONTDUE_FONTS: RefCell<HashMap<fontique::FamilyId, Rc<fontdue::Font>>> = Default::default();
static FONTDUE_FONTS: RefCell<HashMap<(u64, u32), Rc<fontdue::Font>>> = Default::default();
}
fn get_or_create_fontdue_font(font: &fontique::QueryFont) -> Rc<fontdue::Font> {
pub fn get_or_create_fontdue_font_from_blob_and_index(
blob: &fontique::Blob<u8>,
index: u32,
) -> Rc<fontdue::Font> {
FONTDUE_FONTS.with(|font_cache| {
font_cache
.borrow_mut()
.entry(font.family.0)
.entry((blob.id(), index))
.or_insert_with(move || {
fontdue::Font::from_bytes(
font.blob.data(),
blob.data(),
fontdue::FontSettings {
collection_index: font.index,
collection_index: index,
scale: 40.,
..Default::default()
},
@ -38,6 +41,10 @@ fn get_or_create_fontdue_font(font: &fontique::QueryFont) -> Rc<fontdue::Font> {
})
}
fn get_or_create_fontdue_font(font: &fontique::QueryFont) -> Rc<fontdue::Font> {
get_or_create_fontdue_font_from_blob_and_index(&font.blob, font.index)
}
pub fn match_font(
request: &super::FontRequest,
scale_factor: super::ScaleFactor,

View file

@ -9,29 +9,26 @@ use crate::software_renderer::PhysicalLength;
use crate::textlayout::{Glyph, TextShaper};
use i_slint_common::sharedfontique::fontique;
use super::RenderableGlyph;
use super::RenderableVectorGlyph;
// A length in font design space.
struct FontUnit;
type FontLength = euclid::Length<i32, FontUnit>;
type FontScaleFactor = euclid::Scale<f32, FontUnit, PhysicalPx>;
type GlyphCacheKey = (fontique::FamilyId, PhysicalLength, core::num::NonZeroU16);
type GlyphCacheKey = (u64, u32, PhysicalLength, core::num::NonZeroU16);
struct RenderableGlyphWeightScale;
impl clru::WeightScale<GlyphCacheKey, RenderableGlyph> for RenderableGlyphWeightScale {
fn weight(&self, _: &GlyphCacheKey, value: &RenderableGlyph) -> usize {
match &value.alpha_map {
super::GlyphAlphaMap::Static(_) => 0,
super::GlyphAlphaMap::Shared(data) => data.len(),
}
impl clru::WeightScale<GlyphCacheKey, RenderableVectorGlyph> for RenderableGlyphWeightScale {
fn weight(&self, _: &GlyphCacheKey, value: &RenderableVectorGlyph) -> usize {
value.alpha_map.len()
}
}
type GlyphCache = clru::CLruCache<
GlyphCacheKey,
RenderableGlyph,
RenderableVectorGlyph,
std::collections::hash_map::RandomState,
RenderableGlyphWeightScale,
>;
@ -46,7 +43,8 @@ crate::thread_local!(static GLYPH_CACHE: core::cell::RefCell<GlyphCache> =
);
pub struct VectorFont {
font: fontique::QueryFont,
font_index: u32,
font_blob: fontique::Blob<u8>,
fontdue_font: Rc<fontdue::Font>,
ascender: PhysicalLength,
descender: PhysicalLength,
@ -63,7 +61,16 @@ impl VectorFont {
fontdue_font: Rc<fontdue::Font>,
pixel_size: PhysicalLength,
) -> Self {
let face = rustybuzz::ttf_parser::Face::parse(font.blob.data(), font.index).unwrap();
Self::new_from_blob_and_index(font.blob, font.index, fontdue_font, pixel_size)
}
pub fn new_from_blob_and_index(
font_blob: fontique::Blob<u8>,
font_index: u32,
fontdue_font: Rc<fontdue::Font>,
pixel_size: PhysicalLength,
) -> Self {
let face = rustybuzz::ttf_parser::Face::parse(font_blob.data(), font_index).unwrap();
let ascender = FontLength::new(face.ascender() as _);
let descender = FontLength::new(face.descender() as _);
@ -73,7 +80,8 @@ impl VectorFont {
let units_per_em = face.units_per_em();
let scale = FontScaleFactor::new(pixel_size.get() as f32 / units_per_em as f32);
Self {
font,
font_index,
font_blob,
fontdue_font,
ascender: (ascender.cast() * scale).cast(),
descender: (descender.cast() * scale).cast(),
@ -84,6 +92,39 @@ impl VectorFont {
cap_height: (cap_height.cast() * scale).cast(),
}
}
pub fn render_vector_glyph(
&self,
glyph_id: core::num::NonZeroU16,
) -> Option<RenderableVectorGlyph> {
GLYPH_CACHE.with(|cache| {
let mut cache = cache.borrow_mut();
let cache_key = (self.font_blob.id(), self.font_index, self.pixel_size, glyph_id);
if let Some(entry) = cache.get(&cache_key) {
Some(entry.clone())
} else {
let (metrics, alpha_map) =
self.fontdue_font.rasterize_indexed(glyph_id.get(), self.pixel_size.get() as _);
let alpha_map: Rc<[u8]> = alpha_map.into();
let glyph = super::RenderableVectorGlyph {
x: Fixed::from_integer(metrics.xmin.try_into().unwrap()),
y: Fixed::from_integer(metrics.ymin.try_into().unwrap()),
width: PhysicalLength::new(metrics.width.try_into().unwrap()),
height: PhysicalLength::new(metrics.height.try_into().unwrap()),
alpha_map,
pixel_stride: metrics.width.try_into().unwrap(),
bounds: metrics.bounds,
};
cache.put_with_weight(cache_key, glyph.clone()).ok();
Some(glyph)
}
})
}
}
impl TextShaper for VectorFont {
@ -98,7 +139,7 @@ impl TextShaper for VectorFont {
buffer.push_str(text);
let face =
rustybuzz::ttf_parser::Face::parse(self.font.blob.data(), self.font.index).unwrap();
rustybuzz::ttf_parser::Face::parse(self.font_blob.data(), self.font_index).unwrap();
let rb_face = rustybuzz::Face::from_face(face);
let glyph_buffer = rustybuzz::shape(&rb_face, &[], buffer);
@ -129,7 +170,7 @@ impl TextShaper for VectorFont {
fn glyph_for_char(&self, ch: char) -> Option<Glyph<PhysicalLength>> {
let face =
rustybuzz::ttf_parser::Face::parse(self.font.blob.data(), self.font.index).unwrap();
rustybuzz::ttf_parser::Face::parse(self.font_blob.data(), self.font_index).unwrap();
face.glyph_index(ch).map(|glyph_index| {
let mut out_glyph = Glyph::default();
@ -174,32 +215,14 @@ impl crate::textlayout::FontMetrics<PhysicalLength> for VectorFont {
impl super::GlyphRenderer for VectorFont {
fn render_glyph(&self, glyph_id: core::num::NonZeroU16) -> Option<super::RenderableGlyph> {
GLYPH_CACHE.with(|cache| {
let mut cache = cache.borrow_mut();
let cache_key = (self.font.family.0, self.pixel_size, glyph_id);
if let Some(entry) = cache.get(&cache_key) {
Some(entry.clone())
} else {
let (metrics, alpha_map) =
self.fontdue_font.rasterize_indexed(glyph_id.get(), self.pixel_size.get() as _);
let alpha_map: Rc<[u8]> = alpha_map.into();
let glyph = super::RenderableGlyph {
x: Fixed::from_integer(metrics.xmin.try_into().unwrap()),
y: Fixed::from_integer(metrics.ymin.try_into().unwrap()),
width: PhysicalLength::new(metrics.width.try_into().unwrap()),
height: PhysicalLength::new(metrics.height.try_into().unwrap()),
alpha_map: alpha_map.into(),
sdf: false,
pixel_stride: metrics.width.try_into().unwrap(),
};
cache.put_with_weight(cache_key, glyph.clone()).ok();
Some(glyph)
}
self.render_vector_glyph(glyph_id).map(|glyph| super::RenderableGlyph {
x: glyph.x,
y: glyph.y,
width: glyph.width,
height: glyph.height,
alpha_map: glyph.alpha_map.into(),
pixel_stride: glyph.pixel_stride,
sdf: false,
})
}