mirror of
https://github.com/penpot/penpot.git
synced 2026-04-02 22:51:07 -04:00
🎉 Improve text inner stroke rendering
This commit is contained in:
committed by
Belén Albeza
parent
cbe3a3f33e
commit
68760c8e26
@@ -26,7 +26,7 @@ use crate::error::{Error, Result};
|
||||
use crate::performance;
|
||||
use crate::shapes::{
|
||||
all_with_ancestors, radius_to_sigma, Blur, BlurType, Corners, Fill, Shadow, Shape, SolidColor,
|
||||
Stroke, Type,
|
||||
Stroke, StrokeKind, Type,
|
||||
};
|
||||
use crate::state::{ShapesPoolMutRef, ShapesPoolRef};
|
||||
use crate::tiles::{self, PendingTiles, TileRect};
|
||||
@@ -1030,6 +1030,8 @@ impl RenderState {
|
||||
let text_stroke_blur_outset =
|
||||
Stroke::max_bounds_width(shape.visible_strokes(), false);
|
||||
let mut paragraph_builders = text_content.paragraph_builder_group_from_text(None);
|
||||
let stroke_kinds: Vec<StrokeKind> =
|
||||
shape.visible_strokes().rev().map(|s| s.kind).collect();
|
||||
let (mut stroke_paragraphs_list, stroke_opacities): (Vec<_>, Vec<_>) = shape
|
||||
.visible_strokes()
|
||||
.rev()
|
||||
@@ -1038,7 +1040,6 @@ impl RenderState {
|
||||
&text_content,
|
||||
stroke,
|
||||
&shape.selrect(),
|
||||
count_inner_strokes,
|
||||
None,
|
||||
)
|
||||
})
|
||||
@@ -1057,22 +1058,41 @@ impl RenderState {
|
||||
None,
|
||||
)?;
|
||||
|
||||
for (stroke_paragraphs, layer_opacity) in stroke_paragraphs_list
|
||||
for (i, (stroke_paragraphs, layer_opacity)) in stroke_paragraphs_list
|
||||
.iter_mut()
|
||||
.zip(stroke_opacities.iter())
|
||||
.enumerate()
|
||||
{
|
||||
text::render_with_bounds_outset(
|
||||
Some(self),
|
||||
None,
|
||||
&shape,
|
||||
stroke_paragraphs,
|
||||
Some(strokes_surface_id),
|
||||
None,
|
||||
None,
|
||||
text_stroke_blur_outset,
|
||||
None,
|
||||
*layer_opacity,
|
||||
)?;
|
||||
if stroke_kinds[i] == StrokeKind::Inner {
|
||||
let mut mask_builders = text_content.paragraph_builder_group_opaque();
|
||||
let mut fill_builders =
|
||||
text_content.paragraph_builder_group_from_text(None);
|
||||
text::render_inner_stroke(
|
||||
Some(self),
|
||||
None,
|
||||
&shape,
|
||||
&mut mask_builders,
|
||||
stroke_paragraphs,
|
||||
&mut fill_builders,
|
||||
Some(strokes_surface_id),
|
||||
None,
|
||||
text_stroke_blur_outset,
|
||||
*layer_opacity,
|
||||
)?;
|
||||
} else {
|
||||
text::render_with_bounds_outset(
|
||||
Some(self),
|
||||
None,
|
||||
&shape,
|
||||
stroke_paragraphs,
|
||||
Some(strokes_surface_id),
|
||||
None,
|
||||
None,
|
||||
text_stroke_blur_outset,
|
||||
None,
|
||||
*layer_opacity,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut drop_shadows = shape.drop_shadow_paints();
|
||||
@@ -1096,7 +1116,6 @@ impl RenderState {
|
||||
&text_content,
|
||||
stroke,
|
||||
&shape.selrect(),
|
||||
count_inner_strokes,
|
||||
Some(true),
|
||||
)
|
||||
})
|
||||
@@ -1126,6 +1145,8 @@ impl RenderState {
|
||||
text_drop_shadows_surface_id.into(),
|
||||
&parent_shadows,
|
||||
&blur_filter,
|
||||
&stroke_kinds,
|
||||
&text_content,
|
||||
)?;
|
||||
}
|
||||
} else {
|
||||
@@ -1168,25 +1189,47 @@ impl RenderState {
|
||||
text_drop_shadows_surface_id.into(),
|
||||
&drop_shadows,
|
||||
&blur_filter,
|
||||
&stroke_kinds,
|
||||
&text_content,
|
||||
)?;
|
||||
|
||||
// 4. Stroke fills
|
||||
for (stroke_paragraphs, layer_opacity) in stroke_paragraphs_list
|
||||
for (i, (stroke_paragraphs, layer_opacity)) in stroke_paragraphs_list
|
||||
.iter_mut()
|
||||
.zip(stroke_opacities.iter())
|
||||
.enumerate()
|
||||
{
|
||||
text::render_with_bounds_outset(
|
||||
Some(self),
|
||||
None,
|
||||
&shape,
|
||||
stroke_paragraphs,
|
||||
Some(strokes_surface_id),
|
||||
None,
|
||||
blur_filter.as_ref(),
|
||||
text_stroke_blur_outset,
|
||||
None,
|
||||
*layer_opacity,
|
||||
)?;
|
||||
if stroke_kinds[i] == StrokeKind::Inner {
|
||||
let mut mask_builders =
|
||||
text_content.paragraph_builder_group_opaque();
|
||||
let mut fill_builders =
|
||||
text_content.paragraph_builder_group_from_text(None);
|
||||
text::render_inner_stroke(
|
||||
Some(self),
|
||||
None,
|
||||
&shape,
|
||||
&mut mask_builders,
|
||||
stroke_paragraphs,
|
||||
&mut fill_builders,
|
||||
Some(strokes_surface_id),
|
||||
blur_filter.as_ref(),
|
||||
text_stroke_blur_outset,
|
||||
*layer_opacity,
|
||||
)?;
|
||||
} else {
|
||||
text::render_with_bounds_outset(
|
||||
Some(self),
|
||||
None,
|
||||
&shape,
|
||||
stroke_paragraphs,
|
||||
Some(strokes_surface_id),
|
||||
None,
|
||||
blur_filter.as_ref(),
|
||||
text_stroke_blur_outset,
|
||||
None,
|
||||
*layer_opacity,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Stroke inner shadows
|
||||
@@ -1198,6 +1241,8 @@ impl RenderState {
|
||||
Some(innershadows_surface_id),
|
||||
&inner_shadows,
|
||||
&blur_filter,
|
||||
&stroke_kinds,
|
||||
&text_content,
|
||||
)?;
|
||||
|
||||
// 6. Fill Inner shadows
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::{RenderState, SurfaceId};
|
||||
use crate::render::strokes;
|
||||
use crate::shapes::{ParagraphBuilderGroup, Shadow, Shape, Stroke, Type};
|
||||
use crate::shapes::{ParagraphBuilderGroup, Shadow, Shape, Stroke, StrokeKind, TextContent, Type};
|
||||
use skia_safe::{canvas::SaveLayerRec, Paint, Path};
|
||||
|
||||
use crate::error::Result;
|
||||
@@ -127,6 +127,7 @@ fn render_shadow_paint(
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn render_text_shadows(
|
||||
render_state: &mut RenderState,
|
||||
shape: &Shape,
|
||||
@@ -135,6 +136,8 @@ pub fn render_text_shadows(
|
||||
surface_id: Option<SurfaceId>,
|
||||
shadows: &[Paint],
|
||||
blur_filter: &Option<skia_safe::ImageFilter>,
|
||||
stroke_kinds: &[StrokeKind],
|
||||
text_content: &TextContent,
|
||||
) -> Result<()> {
|
||||
if stroke_paragraphs_group.is_empty() {
|
||||
return Ok(());
|
||||
@@ -160,18 +163,35 @@ pub fn render_text_shadows(
|
||||
None,
|
||||
)?;
|
||||
|
||||
for stroke_paragraphs in stroke_paragraphs_group.iter_mut() {
|
||||
text::render(
|
||||
None,
|
||||
Some(canvas),
|
||||
shape,
|
||||
stroke_paragraphs,
|
||||
surface_id,
|
||||
None,
|
||||
blur_filter.as_ref(),
|
||||
None,
|
||||
None,
|
||||
)?;
|
||||
for (i, stroke_paragraphs) in stroke_paragraphs_group.iter_mut().enumerate() {
|
||||
if i < stroke_kinds.len() && stroke_kinds[i] == StrokeKind::Inner {
|
||||
let mut mask_builders = text_content.paragraph_builder_group_opaque();
|
||||
let mut fill_builders = text_content.paragraph_builder_group_from_text(Some(true));
|
||||
text::render_inner_stroke(
|
||||
None,
|
||||
Some(canvas),
|
||||
shape,
|
||||
&mut mask_builders,
|
||||
stroke_paragraphs,
|
||||
&mut fill_builders,
|
||||
surface_id,
|
||||
blur_filter.as_ref(),
|
||||
0.0,
|
||||
None,
|
||||
)?;
|
||||
} else {
|
||||
text::render(
|
||||
None,
|
||||
Some(canvas),
|
||||
shape,
|
||||
stroke_paragraphs,
|
||||
surface_id,
|
||||
None,
|
||||
blur_filter.as_ref(),
|
||||
None,
|
||||
None,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
canvas.restore();
|
||||
|
||||
@@ -3,8 +3,8 @@ use crate::{
|
||||
error::Result,
|
||||
math::Rect,
|
||||
shapes::{
|
||||
calculate_position_data, calculate_text_layout_data, merge_fills, set_paint_fill,
|
||||
ParagraphBuilderGroup, Stroke, StrokeKind, TextContent,
|
||||
calculate_position_data, calculate_text_layout_data, set_paint_fill, ParagraphBuilderGroup,
|
||||
Stroke, StrokeKind, TextContent,
|
||||
},
|
||||
utils::{get_fallback_fonts, get_font_collection},
|
||||
};
|
||||
@@ -19,7 +19,6 @@ pub fn stroke_paragraph_builder_group_from_text(
|
||||
text_content: &TextContent,
|
||||
stroke: &Stroke,
|
||||
bounds: &Rect,
|
||||
count_inner_strokes: usize,
|
||||
use_shadow: Option<bool>,
|
||||
) -> (Vec<ParagraphBuilderGroup>, Option<f32>) {
|
||||
let fallback_fonts = get_fallback_fonts();
|
||||
@@ -33,14 +32,8 @@ pub fn stroke_paragraph_builder_group_from_text(
|
||||
std::collections::HashMap::new();
|
||||
|
||||
for span in paragraph.children().iter() {
|
||||
let text_paint: skia_safe::Handle<_> = merge_fills(span.fills(), *bounds);
|
||||
let (stroke_paints, stroke_layer_opacity) = get_text_stroke_paints(
|
||||
stroke,
|
||||
bounds,
|
||||
&text_paint,
|
||||
count_inner_strokes,
|
||||
remove_stroke_alpha,
|
||||
);
|
||||
let (stroke_paints, stroke_layer_opacity) =
|
||||
get_text_stroke_paints(stroke, bounds, remove_stroke_alpha);
|
||||
|
||||
if group_layer_opacity.is_none() {
|
||||
group_layer_opacity = stroke_layer_opacity;
|
||||
@@ -79,8 +72,6 @@ pub fn stroke_paragraph_builder_group_from_text(
|
||||
fn get_text_stroke_paints(
|
||||
stroke: &Stroke,
|
||||
bounds: &Rect,
|
||||
text_paint: &Paint,
|
||||
count_inner_strokes: usize,
|
||||
remove_stroke_alpha: bool,
|
||||
) -> (Vec<Paint>, Option<f32>) {
|
||||
let mut paints = Vec::new();
|
||||
@@ -104,56 +95,19 @@ fn get_text_stroke_paints(
|
||||
|
||||
match stroke.kind {
|
||||
StrokeKind::Inner => {
|
||||
let shader = text_paint.shader();
|
||||
let mut is_opaque = true;
|
||||
|
||||
if let Some(shader) = shader {
|
||||
is_opaque = shader.is_opaque();
|
||||
}
|
||||
|
||||
if is_opaque && count_inner_strokes == 1 {
|
||||
let mut paint = text_paint.clone();
|
||||
paint.set_style(skia::PaintStyle::Fill);
|
||||
paint.set_anti_alias(true);
|
||||
paints.push(paint);
|
||||
|
||||
let mut paint = skia::Paint::default();
|
||||
paint.set_style(skia::PaintStyle::Stroke);
|
||||
paint.set_blend_mode(skia::BlendMode::SrcIn);
|
||||
paint.set_anti_alias(true);
|
||||
paint.set_stroke_width(stroke.width * 2.0);
|
||||
fill_for_paint(&mut paint);
|
||||
paints.push(paint);
|
||||
} else {
|
||||
let mut paint = skia::Paint::default();
|
||||
if remove_stroke_alpha {
|
||||
paint.set_color(skia::Color::BLACK);
|
||||
paint.set_alpha(255);
|
||||
} else {
|
||||
paint = text_paint.clone();
|
||||
if needs_opacity_layer {
|
||||
let opaque_fill = stroke.fill.with_full_opacity();
|
||||
set_paint_fill(&mut paint, &opaque_fill, bounds, false);
|
||||
} else {
|
||||
set_paint_fill(&mut paint, &stroke.fill, bounds, false);
|
||||
}
|
||||
}
|
||||
|
||||
paint.set_style(skia::PaintStyle::Fill);
|
||||
paint.set_anti_alias(false);
|
||||
paints.push(paint);
|
||||
|
||||
let mut paint = skia::Paint::default();
|
||||
let image_filter =
|
||||
skia_safe::image_filters::erode((stroke.width, stroke.width), None, None);
|
||||
|
||||
paint.set_image_filter(image_filter);
|
||||
paint.set_anti_alias(false);
|
||||
// Just the stroke paint — mask+SrcIn+DstOver layering is handled
|
||||
// by render_inner_stroke_on_canvas.
|
||||
let mut paint = skia::Paint::default();
|
||||
paint.set_style(skia::PaintStyle::Stroke);
|
||||
paint.set_anti_alias(true);
|
||||
paint.set_stroke_width(stroke.width * 2.0);
|
||||
if remove_stroke_alpha {
|
||||
paint.set_color(skia::Color::BLACK);
|
||||
paint.set_alpha(255);
|
||||
paint.set_blend_mode(skia::BlendMode::DstOut);
|
||||
paints.push(paint);
|
||||
} else {
|
||||
fill_for_paint(&mut paint);
|
||||
}
|
||||
paints.push(paint);
|
||||
}
|
||||
StrokeKind::Center => {
|
||||
let mut paint = skia::Paint::default();
|
||||
@@ -330,25 +284,16 @@ fn render_text_on_canvas(
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
fn draw_text(
|
||||
/// Lays out and paints paragraph builders without any layer management.
|
||||
fn paint_text(
|
||||
canvas: &Canvas,
|
||||
shape: &Shape,
|
||||
paragraph_builder_groups: &mut [Vec<ParagraphBuilder>],
|
||||
layer_opacity: Option<f32>,
|
||||
) {
|
||||
let text_content = shape.get_text_content();
|
||||
let layout_info =
|
||||
calculate_text_layout_data(shape, text_content, paragraph_builder_groups, true);
|
||||
|
||||
if let Some(opacity) = layer_opacity {
|
||||
let mut opacity_paint = Paint::default();
|
||||
opacity_paint.set_alpha_f(opacity);
|
||||
let layer_rec = SaveLayerRec::default().paint(&opacity_paint);
|
||||
canvas.save_layer(&layer_rec);
|
||||
} else {
|
||||
canvas.save_layer(&SaveLayerRec::default());
|
||||
}
|
||||
|
||||
for para in &layout_info.paragraphs {
|
||||
para.paragraph.paint(canvas, (para.x, para.y));
|
||||
for deco in ¶.decorations {
|
||||
@@ -364,6 +309,178 @@ fn draw_text(
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_text(
|
||||
canvas: &Canvas,
|
||||
shape: &Shape,
|
||||
paragraph_builder_groups: &mut [Vec<ParagraphBuilder>],
|
||||
layer_opacity: Option<f32>,
|
||||
) {
|
||||
if let Some(opacity) = layer_opacity {
|
||||
let mut opacity_paint = Paint::default();
|
||||
opacity_paint.set_alpha_f(opacity);
|
||||
let layer_rec = SaveLayerRec::default().paint(&opacity_paint);
|
||||
canvas.save_layer(&layer_rec);
|
||||
} else {
|
||||
canvas.save_layer(&SaveLayerRec::default());
|
||||
}
|
||||
|
||||
paint_text(canvas, shape, paragraph_builder_groups);
|
||||
}
|
||||
|
||||
/// Renders an inner stroke using mask + SrcIn + DstOver layer structure.
|
||||
///
|
||||
/// Layer structure:
|
||||
/// saveLayer() — outer layer
|
||||
/// saveLayer() — mask group (isolation)
|
||||
/// paint mask — opaque fill as clip mask
|
||||
/// saveLayer(SrcIn) — clips stroke to mask shape
|
||||
/// paint stroke
|
||||
/// restore
|
||||
/// restore
|
||||
/// saveLayer(DstOver) — fill behind the stroke
|
||||
/// paint fill
|
||||
/// restore
|
||||
/// restore
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_inner_stroke_on_canvas(
|
||||
canvas: &Canvas,
|
||||
shape: &Shape,
|
||||
mask_builders: &mut [Vec<ParagraphBuilder>],
|
||||
stroke_builders: &mut [Vec<ParagraphBuilder>],
|
||||
fill_builders: &mut [Vec<ParagraphBuilder>],
|
||||
blur: Option<&ImageFilter>,
|
||||
layer_opacity: Option<f32>,
|
||||
) {
|
||||
if let Some(blur_filter) = blur {
|
||||
let mut blur_paint = Paint::default();
|
||||
blur_paint.set_image_filter(blur_filter.clone());
|
||||
canvas.save_layer(&SaveLayerRec::default().paint(&blur_paint));
|
||||
}
|
||||
|
||||
// Opacity layer wraps the entire composition
|
||||
if let Some(opacity) = layer_opacity {
|
||||
let mut opacity_paint = Paint::default();
|
||||
opacity_paint.set_alpha_f(opacity);
|
||||
canvas.save_layer(&SaveLayerRec::default().paint(&opacity_paint));
|
||||
}
|
||||
|
||||
// Outer layer
|
||||
canvas.save_layer(&SaveLayerRec::default());
|
||||
|
||||
// Mask group layer (isolates mask from parent surface content)
|
||||
canvas.save_layer(&SaveLayerRec::default());
|
||||
|
||||
// Draw opaque mask (full alpha text shape)
|
||||
paint_text(canvas, shape, mask_builders);
|
||||
|
||||
// SrcIn layer — only keeps stroke pixels where mask has alpha
|
||||
let mut src_in_paint = Paint::default();
|
||||
src_in_paint.set_blend_mode(skia::BlendMode::SrcIn);
|
||||
canvas.save_layer(&SaveLayerRec::default().paint(&src_in_paint));
|
||||
|
||||
// Draw stroke
|
||||
paint_text(canvas, shape, stroke_builders);
|
||||
|
||||
canvas.restore(); // SrcIn layer
|
||||
canvas.restore(); // mask group layer
|
||||
|
||||
// Fill with DstOver (behind the stroke result)
|
||||
let mut dst_over_paint = Paint::default();
|
||||
dst_over_paint.set_blend_mode(skia::BlendMode::DstOver);
|
||||
canvas.save_layer(&SaveLayerRec::default().paint(&dst_over_paint));
|
||||
|
||||
paint_text(canvas, shape, fill_builders);
|
||||
|
||||
canvas.restore(); // DstOver layer
|
||||
canvas.restore(); // outer layer
|
||||
|
||||
if layer_opacity.is_some() {
|
||||
canvas.restore(); // opacity layer
|
||||
}
|
||||
|
||||
if blur.is_some() {
|
||||
canvas.restore(); // blur layer
|
||||
}
|
||||
}
|
||||
|
||||
/// Public API for rendering inner strokes with mask+SrcIn+DstOver approach.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn render_inner_stroke(
|
||||
render_state: Option<&mut RenderState>,
|
||||
canvas: Option<&Canvas>,
|
||||
shape: &Shape,
|
||||
mask_builders: &mut [Vec<ParagraphBuilder>],
|
||||
stroke_builders: &mut [Vec<ParagraphBuilder>],
|
||||
fill_builders: &mut [Vec<ParagraphBuilder>],
|
||||
surface_id: Option<SurfaceId>,
|
||||
blur: Option<&ImageFilter>,
|
||||
stroke_bounds_outset: f32,
|
||||
layer_opacity: Option<f32>,
|
||||
) -> Result<()> {
|
||||
if let Some(render_state) = render_state {
|
||||
let target_surface = surface_id.unwrap_or(SurfaceId::Fills);
|
||||
|
||||
if let Some(blur_filter) = blur {
|
||||
let mut text_bounds = shape
|
||||
.get_text_content()
|
||||
.calculate_bounds(shape, false)
|
||||
.to_rect();
|
||||
if stroke_bounds_outset > 0.0 {
|
||||
text_bounds.inset((-stroke_bounds_outset, -stroke_bounds_outset));
|
||||
}
|
||||
let bounds = blur_filter.compute_fast_bounds(text_bounds);
|
||||
if bounds.is_finite() && bounds.width() > 0.0 && bounds.height() > 0.0 {
|
||||
let blur_filter_clone = blur_filter.clone();
|
||||
if filters::render_with_filter_surface(
|
||||
render_state,
|
||||
bounds,
|
||||
target_surface,
|
||||
|state, temp_surface| {
|
||||
let temp_canvas = state.surfaces.canvas(temp_surface);
|
||||
render_inner_stroke_on_canvas(
|
||||
temp_canvas,
|
||||
shape,
|
||||
mask_builders,
|
||||
stroke_builders,
|
||||
fill_builders,
|
||||
Some(&blur_filter_clone),
|
||||
layer_opacity,
|
||||
);
|
||||
Ok(())
|
||||
},
|
||||
)? {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let canvas = render_state.surfaces.canvas_and_mark_dirty(target_surface);
|
||||
render_inner_stroke_on_canvas(
|
||||
canvas,
|
||||
shape,
|
||||
mask_builders,
|
||||
stroke_builders,
|
||||
fill_builders,
|
||||
blur,
|
||||
layer_opacity,
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(canvas) = canvas {
|
||||
render_inner_stroke_on_canvas(
|
||||
canvas,
|
||||
shape,
|
||||
mask_builders,
|
||||
stroke_builders,
|
||||
fill_builders,
|
||||
blur,
|
||||
layer_opacity,
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw_text_decorations(
|
||||
canvas: &Canvas,
|
||||
text_style: &TextStyle,
|
||||
|
||||
@@ -605,6 +605,40 @@ impl TextContent {
|
||||
paragraph_group
|
||||
}
|
||||
|
||||
/// Creates paragraph builders with always-opaque paint (BLACK @ alpha 255).
|
||||
/// Used as a clip mask for inner stroke rendering.
|
||||
pub fn paragraph_builder_group_opaque(&self) -> Vec<ParagraphBuilderGroup> {
|
||||
let fonts = get_font_collection();
|
||||
let fallback_fonts = get_fallback_fonts();
|
||||
let mut paragraph_group = Vec::new();
|
||||
|
||||
for paragraph in self.paragraphs() {
|
||||
let paragraph_style = paragraph.paragraph_to_style();
|
||||
let mut builder = ParagraphBuilder::new(¶graph_style, fonts);
|
||||
let mut has_text = false;
|
||||
for span in paragraph.children() {
|
||||
let text_style = span.to_style(
|
||||
&self.bounds(),
|
||||
fallback_fonts,
|
||||
true, // always opaque
|
||||
paragraph.line_height(),
|
||||
);
|
||||
let text: String = span.apply_text_transform();
|
||||
if !text.is_empty() {
|
||||
has_text = true;
|
||||
}
|
||||
builder.push_style(&text_style);
|
||||
builder.add_text(&text);
|
||||
}
|
||||
if !has_text {
|
||||
builder.add_text(" ");
|
||||
}
|
||||
paragraph_group.push(vec![builder]);
|
||||
}
|
||||
|
||||
paragraph_group
|
||||
}
|
||||
|
||||
/// Performs an Auto Width text layout.
|
||||
fn text_layout_auto_width(&self) -> TextContentLayoutResult {
|
||||
let mut paragraph_builders = self.paragraph_builder_group_from_text(None);
|
||||
@@ -1094,6 +1128,7 @@ impl TextSpan {
|
||||
self.text = text;
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn fills(&self) -> &[shapes::Fill] {
|
||||
&self.fills
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user