use crate::{
App, Bounds, InlineLayout, LineLayout, PaintFragment, PaintQuad, Pixels, Point, Result,
ShapedTextLayout, SharedString, TextAlign, TextSystem, VisualLine, Window, point, size,
};
use derive_more::{Deref, DerefMut};
use std::sync::{
Arc,
atomic::{AtomicUsize, Ordering},
};
static GLYPH_PAINT_ERRORS: AtomicUsize = AtomicUsize::new(0);
const MAX_LOGGED_GLYPH_PAINT_ERRORS: usize = 20;
#[derive(Clone, Debug, Deref, DerefMut)]
pub struct ShapedLine {
#[deref]
#[deref_mut]
pub(crate) layout: Arc<LineLayout>,
pub text: SharedString,
}
impl ShapedLine {
#[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> usize {
self.layout.len
}
pub fn width(&self) -> Pixels {
self.layout.width
}
pub fn paint(
&self,
origin: Point<Pixels>,
line_height: Pixels,
align: TextAlign,
align_width: Option<Pixels>,
window: &mut Window,
cx: &mut App,
) -> Result<()> {
paint_visual_text(
origin,
&self.layout,
line_height,
align,
align_width,
&self.layout.visual_lines,
TextPaintPass::Foreground,
window,
cx,
)
}
pub fn paint_background(
&self,
origin: Point<Pixels>,
line_height: Pixels,
align: TextAlign,
align_width: Option<Pixels>,
window: &mut Window,
cx: &mut App,
) -> Result<()> {
paint_visual_text(
origin,
&self.layout,
line_height,
align,
align_width,
&self.layout.visual_lines,
TextPaintPass::Background,
window,
cx,
)
}
}
#[derive(Debug, Deref, DerefMut)]
pub struct ShapedText {
#[deref]
#[deref_mut]
pub(crate) layout: Arc<ShapedTextLayout>,
pub text: SharedString,
}
impl ShapedText {
#[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> usize {
self.layout.len()
}
pub fn paint(
&self,
origin: Point<Pixels>,
line_height: Pixels,
align: TextAlign,
bounds: Option<Bounds<Pixels>>,
window: &mut Window,
cx: &mut App,
) -> Result<()> {
let align_width = match bounds {
Some(bounds) => Some(bounds.size.width),
None => self.layout.wrap_width,
};
paint_visual_text(
origin,
&self.layout.layout,
line_height,
align,
align_width,
&self.layout.visual_lines,
TextPaintPass::Foreground,
window,
cx,
)
}
pub fn paint_background(
&self,
origin: Point<Pixels>,
line_height: Pixels,
align: TextAlign,
bounds: Option<Bounds<Pixels>>,
window: &mut Window,
cx: &mut App,
) -> Result<()> {
let align_width = match bounds {
Some(bounds) => Some(bounds.size.width),
None => self.layout.wrap_width,
};
paint_visual_text(
origin,
&self.layout.layout,
line_height,
align,
align_width,
&self.layout.visual_lines,
TextPaintPass::Background,
window,
cx,
)
}
}
impl InlineLayout {
pub fn paint_background(
&self,
origin: Point<Pixels>,
window: &mut Window,
cx: &mut App,
) -> Result<()> {
paint_inline_layout(self, origin, TextPaintPass::Background, window, cx)
}
pub fn paint(&self, origin: Point<Pixels>, window: &mut Window, cx: &mut App) -> Result<()> {
paint_inline_layout(self, origin, TextPaintPass::Foreground, window, cx)
}
}
#[derive(Clone, Copy, PartialEq)]
enum TextPaintPass {
Background,
Foreground,
}
struct FragmentPaintContext<'a> {
line_origin: Point<Pixels>,
line_height: Pixels,
baseline_y: Pixels,
layout: &'a LineLayout,
pass: TextPaintPass,
text_system: &'a TextSystem,
}
fn paint_inline_layout(
inline: &InlineLayout,
origin: Point<Pixels>,
pass: TextPaintPass,
window: &mut Window,
cx: &mut App,
) -> Result<()> {
if inline.lines.is_empty() {
return Ok(());
}
let text_system = cx.text_system().clone();
let placement = place_inline_layout(origin, inline.alignment_offset, window);
window.paint_layer(Bounds::new(placement.origin, inline.size), |window| {
for (line, visual_line) in inline.lines.iter().zip(&inline.layout.visual_lines) {
let line_origin = origin + line.origin + placement.delta;
paint_visual_line(
&inline.layout,
visual_line,
line_origin,
line.size.height,
line_origin.y + line.baseline,
pass,
&text_system,
window,
);
}
Ok(())
})
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct InlineLayoutPlacement {
pub(crate) origin: Point<Pixels>,
pub(crate) delta: Point<Pixels>,
}
pub(crate) fn place_inline_layout(
content_origin: Point<Pixels>,
alignment_offset: Pixels,
window: &Window,
) -> InlineLayoutPlacement {
let anchor = content_origin + point(alignment_offset, Pixels::ZERO);
let placed_anchor = window.pixel_snap_point(anchor);
let delta = placed_anchor - anchor;
InlineLayoutPlacement {
origin: content_origin + delta,
delta,
}
}
fn paint_visual_line(
layout: &LineLayout,
visual_line: &VisualLine,
line_origin: Point<Pixels>,
line_height: Pixels,
baseline_y: Pixels,
pass: TextPaintPass,
text_system: &TextSystem,
window: &mut Window,
) {
let context = FragmentPaintContext {
line_origin,
line_height,
baseline_y,
layout,
pass,
text_system,
};
for fragment in &layout.paint_fragments[visual_line.paint_fragment_range.clone()] {
paint_text_fragment(fragment, &context, window);
}
}
fn paint_text_fragment(
fragment: &PaintFragment,
context: &FragmentPaintContext<'_>,
window: &mut Window,
) {
paint_fragment_decorations_at(
fragment,
context.line_origin,
context.line_height,
context.baseline_y,
context.layout,
window,
context.pass == TextPaintPass::Foreground,
);
if context.pass == TextPaintPass::Background {
return;
}
let max_glyph_size = context
.text_system
.bounding_box(fragment.font_id, fragment.font_size)
.size;
for glyph in &*fragment.glyphs {
let cull_origin = point(
context.line_origin.x + glyph.position.x,
context.line_origin.y,
);
if !Bounds::new(cull_origin, max_glyph_size).intersects(&window.content_mask().bounds) {
continue;
}
let glyph_origin = point(
context.line_origin.x + glyph.position.x,
context.baseline_y + glyph.position.y,
);
let result = if glyph.is_emoji {
window.paint_emoji_with_color(
glyph_origin,
fragment.font_id,
glyph.id,
fragment.font_size,
fragment.style.color,
)
} else {
window.paint_glyph(
glyph_origin,
fragment.font_id,
glyph.id,
fragment.font_size,
fragment.style.color,
)
};
if let Err(error) = result {
let error_idx = GLYPH_PAINT_ERRORS.fetch_add(1, Ordering::Relaxed);
if error_idx < MAX_LOGGED_GLYPH_PAINT_ERRORS {
log::error!(
"failed to paint glyph {:?} from font {:?}: {error:#}",
glyph.id,
fragment.font_id
);
} else if error_idx == MAX_LOGGED_GLYPH_PAINT_ERRORS {
log::error!("suppressing further glyph paint errors");
}
}
}
}
fn paint_visual_text(
origin: Point<Pixels>,
layout: &LineLayout,
line_height: Pixels,
align: TextAlign,
align_width: Option<Pixels>,
visual_lines: &[VisualLine],
pass: TextPaintPass,
window: &mut Window,
cx: &mut App,
) -> Result<()> {
if visual_lines.is_empty() {
return Ok(());
}
let line_bounds = visual_text_bounds(origin, line_height, align, align_width, visual_lines);
window.paint_layer(line_bounds, |window| {
let padding_top = (line_height - layout.ascent - layout.descent) / 2.;
let text_system = cx.text_system().clone();
for (line_index, line) in visual_lines.iter().enumerate() {
let line_origin = point(
visual_line_origin_x(origin.x, align, align_width, line),
origin.y + line_index as f32 * line_height,
);
paint_visual_line(
layout,
line,
line_origin,
line_height,
line_origin.y + padding_top + layout.ascent,
pass,
&text_system,
window,
);
}
Ok(())
})
}
fn visual_text_bounds(
origin: Point<Pixels>,
line_height: Pixels,
align: TextAlign,
align_width: Option<Pixels>,
visual_lines: &[VisualLine],
) -> Bounds<Pixels> {
let mut left = Pixels::MAX;
let mut right = Pixels::MIN;
for line in visual_lines {
let line_left = visual_line_origin_x(origin.x, align, align_width, line);
left = left.min(line_left);
right = right.max(line_left + line.advance_width);
}
Bounds::new(
point(left, origin.y),
size(right - left, line_height * visual_lines.len() as f32),
)
}
fn visual_line_origin_x(
origin_x: Pixels,
align: TextAlign,
align_width: Option<Pixels>,
line: &VisualLine,
) -> Pixels {
line.offset
+ aligned_visual_origin_x(
origin_x,
align_width.unwrap_or(line.advance_width),
line.advance_width,
align,
line.direction,
)
}
fn paint_fragment_decorations_at(
fragment: &PaintFragment,
line_origin: Point<Pixels>,
line_height: Pixels,
baseline_y: Pixels,
layout: &LineLayout,
window: &mut Window,
foreground: bool,
) {
let range = line_origin.x + fragment.x_range.start..line_origin.x + fragment.x_range.end;
if foreground {
if let Some(mut underline) = fragment.style.underline {
underline.color = Some(underline.color.unwrap_or(fragment.style.color));
window.paint_underline(
point(
range.start,
baseline_y + fragment.underline_offset.unwrap_or(layout.descent * 0.618),
),
range.end - range.start,
&underline,
);
}
if let Some(mut strike) = fragment.style.strikethrough {
strike.color = Some(strike.color.unwrap_or(fragment.style.color));
let strike_y = baseline_y
+ fragment
.strikethrough_offset
.unwrap_or(-layout.ascent * 0.5);
window.paint_strikethrough(
point(range.start, strike_y),
range.end - range.start,
&strike,
);
}
} else if let Some(color) = fragment.style.background_color {
window.paint_quad(
PaintQuad::new(Bounds::new(
point(range.start, line_origin.y),
size(range.end - range.start, line_height),
))
.background(color),
);
}
}
fn aligned_visual_origin_x(
origin_x: Pixels,
align_width: Pixels,
line_width: Pixels,
align: TextAlign,
direction: crate::ResolvedDirection,
) -> Pixels {
match align {
TextAlign::Start if direction.is_rtl() => origin_x + align_width - line_width,
TextAlign::Start | TextAlign::Left => origin_x,
TextAlign::Center => (origin_x * 2.0 + align_width - line_width) / 2.0,
TextAlign::End if direction.is_rtl() => origin_x,
TextAlign::End | TextAlign::Right => origin_x + align_width - line_width,
}
}