use std::{borrow::Cow, cell::RefCell, collections::HashMap, convert::Into};
use parley::{GlyphRun, layout::BreakReason};
use skrifa::color::ColorPalette;
use taffy::{Layout, Point, Size};
use tiny_skia::Pixmap;
use crate::{
Result,
layout::{
inline::{InlineBrush, InlineLayout, ProcessedInlineSpan, break_lines},
style::{
Affine, BlendMode, Color, ImageScalingAlgorithm, SizedFontStyle, TextTransform, TextWrapMode,
WhiteSpaceCollapse,
},
},
rendering::{
BorderProperties, Canvas, ColorTile, Command, MaskSamplingOptions, MaskSourceToPixmapOptions,
PaintSource, Placement, SamplingOptions, Stroke, composite_mask_source_to_pixmap, render_mask,
},
resources::font::{ResolvedColorLayer, ResolvedGlyph},
};
pub(crate) type GlyphMaskCache = HashMap<u64, (Vec<u8>, Placement)>;
const GLYPH_MASK_CACHE_MAX_ENTRIES: usize = 4096;
thread_local! {
static SHARED_GLYPH_MASK_CACHE: RefCell<GlyphMaskCache> = RefCell::new(HashMap::new());
}
pub(crate) fn with_shared_glyph_cache<R>(f: impl FnOnce(&mut GlyphMaskCache) -> R) -> R {
SHARED_GLYPH_MASK_CACHE.with(|c| match c.try_borrow_mut() {
Ok(mut cache) => {
if cache.len() > GLYPH_MASK_CACHE_MAX_ENTRIES {
cache.clear();
}
f(&mut cache)
}
Err(_) => {
let mut local = HashMap::new();
f(&mut local)
}
})
}
fn glyph_cache_key_and_offset(transform: Affine, glyph_signature: u64) -> Option<(u64, i32, i32)> {
if !transform.only_translation() {
return None;
}
let scaled_x = (transform.x * 4.0).round() as i64;
let int_x = scaled_x.div_euclid(4) as i32;
let bucket_x = scaled_x.rem_euclid(4) as u64;
let int_y = transform.y.round() as i32;
let key = (glyph_signature << 2) | bucket_x;
Some((key, int_x, int_y))
}
fn draw_outline_with_cache(
paths: &[Command],
glyph_signature: u64,
transform: Affine,
color: Color,
canvas: &mut Canvas,
) {
let Some((key, int_x, int_y)) = glyph_cache_key_and_offset(transform, glyph_signature) else {
let (mask, placement) = render_mask(paths, Some(transform), None, &mut canvas.buffer_pool);
canvas.draw_mask(&mask, placement, color, BlendMode::Normal);
canvas.buffer_pool.release(mask);
return;
};
with_shared_glyph_cache(|cache| {
let (cached_mask, cached_placement) = cache.entry(key).or_insert_with(|| {
let bucket_x = (key & 3) as f32 * 0.25;
let cache_transform = Affine::translation(bucket_x, 0.0);
render_mask(paths, Some(cache_transform), None, &mut canvas.buffer_pool)
});
let placement = cached_placement.translate(int_x, int_y);
canvas.draw_mask(cached_mask, placement, color, BlendMode::Normal);
});
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct DecorationSegmentParams {
pub(crate) offset: f32,
pub(crate) size: f32,
pub(crate) start_x: f32,
pub(crate) end_x: f32,
pub(crate) layout: Layout,
pub(crate) transform: Affine,
}
pub(crate) fn draw_decoration(
canvas: &mut Canvas,
glyph_run: &GlyphRun<'_, InlineBrush>,
color: Color,
offset: f32,
size: f32,
layout: Layout,
transform: Affine,
) {
let start_x = layout.border.left + layout.padding.left + glyph_run.offset();
let end_x = start_x + glyph_run.advance();
draw_decoration_segment(
canvas,
color,
DecorationSegmentParams {
offset,
size,
start_x,
end_x,
layout,
transform,
},
);
}
pub(crate) fn draw_decoration_segment(
canvas: &mut Canvas,
color: Color,
params: DecorationSegmentParams,
) {
if params.end_x <= params.start_x {
return;
}
let snapped_start_x = params.start_x.floor();
let width = (params.end_x.ceil() - snapped_start_x) as u32;
let tile = ColorTile::new(color.into(), width, params.size as u32);
canvas.overlay_image(
&tile,
BorderProperties::default(),
params.transform
* Affine::translation(
snapped_start_x,
params.layout.border.top + params.layout.padding.top + params.offset,
),
ImageScalingAlgorithm::Auto,
BlendMode::Normal,
);
}
pub(crate) fn draw_glyph_clip_image(
glyph: &ResolvedGlyph,
canvas: &mut Canvas,
style: &SizedFontStyle,
mut transform: Affine,
inline_offset: Point<f32>,
clip_image: PaintSource<'_>,
) -> Result<()> {
transform *= Affine::translation(inline_offset.x, inline_offset.y);
match glyph {
ResolvedGlyph::Bitmap(bitmap) => {
transform *= Affine::translation(bitmap.placement.left as f32, -bitmap.placement.top as f32);
let mask_capacity = (bitmap.placement.width * bitmap.placement.height) as usize;
let mut mask = canvas.buffer_pool.acquire_dirty(mask_capacity);
if mask_capacity > 0 {
let mask_len = mask.len();
let write_len = mask_capacity.min(mask_len);
bitmap.write_alpha_mask(&mut mask[..write_len]);
}
let Some(mut bottom) = Pixmap::new(bitmap.placement.width, bitmap.placement.height) else {
return Ok(());
};
let mut bottom_pixmap = bottom.as_mut();
composite_mask_source_to_pixmap(
&mut bottom_pixmap,
&mask,
clip_image,
MaskSourceToPixmapOptions {
placement: Placement {
left: 0,
top: 0,
width: bitmap.placement.width,
height: bitmap.placement.height,
},
sampling: MaskSamplingOptions {
canvas_to_source: Affine::translation(
inline_offset.x + bitmap.placement.left as f32,
inline_offset.y - bitmap.placement.top as f32,
),
sample_bias: Point::ZERO,
algorithm: ImageScalingAlgorithm::Pixelated,
},
mode: BlendMode::Normal,
combined_mask: None,
},
);
canvas.overlay_sampled_pixmap(
&bottom,
Size {
width: bottom.width(),
height: bottom.height(),
},
BorderProperties::default(),
transform,
SamplingOptions {
logical_to_source: Affine::IDENTITY,
algorithm: ImageScalingAlgorithm::Auto,
},
BlendMode::Normal,
);
canvas.buffer_pool.release(mask);
}
ResolvedGlyph::Outline(outline) => {
let Some(inverse) = transform.invert() else {
return Ok(());
};
let sampling = MaskSamplingOptions {
canvas_to_source: Affine::translation(inline_offset.x, inline_offset.y) * inverse,
sample_bias: Point::ZERO,
algorithm: style.parent.image_rendering,
};
if let Some((key, int_x, int_y)) =
glyph_cache_key_and_offset(transform, outline.cache_signature())
{
with_shared_glyph_cache(|cache| {
let (cached_mask, cached_placement) = cache.entry(key).or_insert_with(|| {
let bucket_x = (key & 3) as f32 * 0.25;
let cache_transform = Affine::translation(bucket_x, 0.0);
render_mask(
outline.paths(),
Some(cache_transform),
None,
&mut canvas.buffer_pool,
)
});
let placement = cached_placement.translate(int_x, int_y);
canvas.composite_mask_source(
cached_mask,
placement,
clip_image,
sampling,
BlendMode::Normal,
);
});
} else {
let (mask, placement) = render_mask(
outline.paths(),
Some(transform),
None,
&mut canvas.buffer_pool,
);
canvas.composite_mask_source(&mask, placement, clip_image, sampling, BlendMode::Normal);
canvas.buffer_pool.release(mask);
}
if let Some(embolden) = outline.embolden() {
draw_text_embolden_clip_image(
canvas,
style,
transform,
outline.paths(),
embolden,
clip_image,
inline_offset,
);
}
draw_text_stroke_clip_image(
canvas,
style,
transform,
outline.paths(),
clip_image,
inline_offset,
);
}
}
Ok(())
}
pub(crate) fn draw_glyph(
glyph: &ResolvedGlyph,
canvas: &mut Canvas,
style: &SizedFontStyle,
mut transform: Affine,
inline_offset: Point<f32>,
color: Color,
palette: Option<&ColorPalette>,
) -> Result<()> {
transform *= Affine::translation(inline_offset.x, inline_offset.y);
match glyph {
ResolvedGlyph::Bitmap(bitmap) => {
transform *= Affine::translation(bitmap.placement.left as f32, -bitmap.placement.top as f32);
transform *= Affine::scale(bitmap.scale_x, bitmap.scale_y);
canvas.overlay_sampled_pixmap(
&bitmap.pixmap,
Size {
width: bitmap.pixmap.width(),
height: bitmap.pixmap.height(),
},
Default::default(),
transform,
SamplingOptions {
logical_to_source: Affine::IDENTITY,
algorithm: Default::default(),
},
BlendMode::Normal,
);
}
ResolvedGlyph::Outline(outline) => {
if let Some(color_layers) = outline.color_layers()
&& let Some(palette) = palette
{
draw_color_outline_image(canvas, color_layers, palette, color, transform);
} else {
draw_outline_with_cache(
outline.paths(),
outline.cache_signature(),
transform,
color,
canvas,
);
}
if let Some(embolden) = outline.embolden() {
draw_text_embolden(canvas, style, transform, outline.paths(), color, embolden);
}
draw_text_stroke(canvas, style, transform, outline.paths());
}
}
Ok(())
}
fn draw_text_stroke_clip_image(
canvas: &mut Canvas,
style: &SizedFontStyle,
transform: Affine,
paths: &[Command],
clip_image: PaintSource<'_>,
inline_offset: Point<f32>,
) {
if style.stroke_width <= 0.0 {
return;
}
let Some(inverse) = transform.invert() else {
return;
};
let scale = transform.uniform_scale().max(f32::EPSILON);
let mut stroke = Stroke::new(style.stroke_width / scale);
stroke.join = style.parent.stroke_linejoin.into();
let (stroke_mask, stroke_placement) = render_mask(
paths,
Some(transform),
Some(stroke.into()),
&mut canvas.buffer_pool,
);
canvas.composite_mask_color_over_source(
&stroke_mask,
stroke_placement,
clip_image,
style.text_stroke_color,
MaskSamplingOptions {
canvas_to_source: Affine::translation(inline_offset.x, inline_offset.y) * inverse,
sample_bias: Point::ZERO,
algorithm: style.parent.image_rendering,
},
BlendMode::Normal,
);
canvas.buffer_pool.release(stroke_mask);
}
fn draw_text_embolden_clip_image(
canvas: &mut Canvas,
style: &SizedFontStyle,
transform: Affine,
paths: &[Command],
embolden: f32,
clip_image: PaintSource<'_>,
inline_offset: Point<f32>,
) {
if embolden <= 0.0 {
return;
}
let Some(inverse) = transform.invert() else {
return;
};
let mut stroke = Stroke::new(embolden * 2.0);
stroke.join = style.parent.stroke_linejoin.into();
let (stroke_mask, stroke_placement) = render_mask(
paths,
Some(transform),
Some(stroke.into()),
&mut canvas.buffer_pool,
);
canvas.composite_mask_source(
&stroke_mask,
stroke_placement,
clip_image,
MaskSamplingOptions {
canvas_to_source: Affine::translation(inline_offset.x, inline_offset.y) * inverse,
sample_bias: Point::ZERO,
algorithm: style.parent.image_rendering,
},
BlendMode::Normal,
);
canvas.buffer_pool.release(stroke_mask);
}
fn draw_text_stroke(
canvas: &mut Canvas,
style: &SizedFontStyle,
transform: Affine,
paths: &[Command],
) {
if style.stroke_width <= 0.0 {
return;
}
let scale = transform.uniform_scale().max(f32::EPSILON);
let mut stroke = Stroke::new(style.stroke_width / scale);
stroke.join = style.parent.stroke_linejoin.into();
let (stroke_mask, stroke_placement) = render_mask(
paths,
Some(transform),
Some(stroke.into()),
&mut canvas.buffer_pool,
);
canvas.draw_mask(
&stroke_mask,
stroke_placement,
style.text_stroke_color,
BlendMode::Normal,
);
canvas.buffer_pool.release(stroke_mask);
}
fn draw_text_embolden(
canvas: &mut Canvas,
style: &SizedFontStyle,
transform: Affine,
paths: &[Command],
color: Color,
embolden: f32,
) {
if embolden <= 0.0 {
return;
}
let mut stroke = Stroke::new(embolden * 2.0);
stroke.join = style.parent.stroke_linejoin.into();
let (stroke_mask, stroke_placement) = render_mask(
paths,
Some(transform),
Some(stroke.into()),
&mut canvas.buffer_pool,
);
canvas.draw_mask(&stroke_mask, stroke_placement, color, BlendMode::Normal);
canvas.buffer_pool.release(stroke_mask);
}
fn draw_text_shadow(
canvas: &mut Canvas,
style: &SizedFontStyle,
transform: Affine,
paths: &[Command],
) -> Result<()> {
if style.text_shadow.is_empty() {
return Ok(());
}
for shadow in style.text_shadow.iter() {
shadow.draw_outset(canvas, paths, transform, Default::default(), None)?;
}
Ok(())
}
pub(crate) fn draw_glyph_text_shadow(
glyph: &ResolvedGlyph,
canvas: &mut Canvas,
style: &SizedFontStyle,
mut transform: Affine,
inline_offset: Point<f32>,
) -> Result<()> {
transform *= Affine::translation(inline_offset.x, inline_offset.y);
if let ResolvedGlyph::Outline(outline) = glyph {
draw_text_shadow(canvas, style, transform, outline.paths())?;
}
Ok(())
}
fn draw_color_outline_image(
canvas: &mut Canvas,
color_layers: &[ResolvedColorLayer],
palette: &ColorPalette,
foreground_color: Color,
transform: Affine,
) {
let foreground_opacity = foreground_color.0[3] as f32 / 255.0;
if foreground_opacity <= 0.0 {
return;
}
for layer in color_layers {
let color = if layer.palette_index == u16::MAX {
let alpha = (foreground_opacity * layer.alpha * 255.0)
.round()
.clamp(0.0, 255.0) as u8;
Color([
foreground_color.0[0],
foreground_color.0[1],
foreground_color.0[2],
alpha,
])
} else {
let Some(record) = palette.colors().get(usize::from(layer.palette_index)) else {
continue;
};
let alpha = ((record.alpha() as f32 / 255.0) * layer.alpha * foreground_opacity * 255.0)
.round()
.clamp(0.0, 255.0) as u8;
Color([record.red(), record.green(), record.blue(), alpha])
};
let (mask, placement) =
render_mask(&layer.paths, Some(transform), None, &mut canvas.buffer_pool);
canvas.draw_mask(&mask, placement, color, BlendMode::Normal);
canvas.buffer_pool.release(mask);
}
}
#[derive(Clone, Copy, Debug)]
pub(crate) enum MaxHeight {
Absolute(f32),
Lines(u32),
HeightAndLines(f32, u32),
}
pub(crate) fn apply_text_transform<'a>(input: &'a str, transform: TextTransform) -> Cow<'a, str> {
match transform {
TextTransform::None => Cow::Borrowed(input),
TextTransform::Uppercase => Cow::Owned(input.to_uppercase()),
TextTransform::Lowercase => Cow::Owned(input.to_lowercase()),
TextTransform::Capitalize => {
let mut result = String::with_capacity(input.len());
let mut start_of_word = true;
for ch in input.chars() {
if ch.is_alphabetic() {
if start_of_word {
result.extend(ch.to_uppercase());
start_of_word = false;
} else {
result.extend(ch.to_lowercase());
}
} else {
start_of_word = !ch.is_numeric();
result.push(ch);
}
}
Cow::Owned(result)
}
}
}
pub(crate) fn apply_white_space_collapse<'a>(
input: &'a str,
collapse: WhiteSpaceCollapse,
previous_collapsible_space: &mut bool,
previous_was_line_break: &mut bool,
) -> Cow<'a, str> {
match collapse {
WhiteSpaceCollapse::Preserve => {
*previous_was_line_break = false;
Cow::Borrowed(input)
}
WhiteSpaceCollapse::Collapse => {
let mut out = String::with_capacity(input.len());
let mut last_was_ws = *previous_collapsible_space;
for ch in input.chars() {
if ch.is_whitespace() {
if !last_was_ws {
out.push(' ');
last_was_ws = true;
}
} else {
out.push(ch);
last_was_ws = false;
}
}
*previous_collapsible_space = last_was_ws;
*previous_was_line_break = false;
Cow::Owned(out)
}
WhiteSpaceCollapse::PreserveSpaces => {
let mut out = String::with_capacity(input.len());
let mut last_was_space = *previous_collapsible_space;
for ch in input.chars() {
if matches!(ch, '\n' | '\r' | '\x0B' | '\x0C' | '\u{2028}' | '\u{2029}') {
if !last_was_space {
out.push(' ');
last_was_space = true;
}
} else {
out.push(ch);
last_was_space = ch == ' ' || ch == '\t';
}
}
*previous_collapsible_space = last_was_space;
*previous_was_line_break = false;
Cow::Owned(out)
}
WhiteSpaceCollapse::PreserveBreaks => {
let mut out = String::with_capacity(input.len());
let mut last_was_space = *previous_collapsible_space;
let mut last_was_line_break = *previous_was_line_break;
for ch in input.chars() {
if ch == ' ' || ch == '\t' {
if last_was_line_break {
continue;
}
if !last_was_space {
out.push(' ');
last_was_space = true;
}
} else {
out.push(ch);
last_was_space = false;
last_was_line_break =
matches!(ch, '\n' | '\r' | '\x0B' | '\x0C' | '\u{2028}' | '\u{2029}');
}
}
*previous_collapsible_space = last_was_space;
*previous_was_line_break = last_was_line_break;
Cow::Owned(out)
}
}
}
fn count_emergency_line_breaks(layout: &InlineLayout) -> usize {
let line_count = layout.lines().count();
layout
.lines()
.take(line_count.saturating_sub(1))
.filter(|line| line.break_reason() == BreakReason::Emergency)
.count()
}
#[derive(Clone, Copy)]
pub(crate) struct RebreakOptions {
pub(crate) max_width: f32,
pub(crate) max_height: Option<MaxHeight>,
pub(crate) line_height_hint: f32,
pub(crate) text_wrap_mode: TextWrapMode,
}
pub(crate) fn make_balanced_text(
inline_layout: &mut InlineLayout,
options: RebreakOptions,
target_lines: usize,
device_pixel_ratio: f32,
spans: &[ProcessedInlineSpan<'_, '_>],
custom_inline_boxes: &mut Vec<parley::PositionedInlineBox>,
) -> bool {
let RebreakOptions {
max_width,
max_height,
line_height_hint,
text_wrap_mode,
} = options;
if target_lines <= 1 {
return false;
}
let initial_emergency_breaks = count_emergency_line_breaks(inline_layout);
let mut left = max_width / 2.0;
let mut right = max_width;
const MAX_ITERATIONS: u32 = 20;
let mut iterations = 0;
while left + device_pixel_ratio < right && iterations < MAX_ITERATIONS {
iterations += 1;
let mid = (left + right) / 2.0;
custom_inline_boxes.clear();
break_lines(
inline_layout,
mid,
None,
line_height_hint,
text_wrap_mode,
spans,
custom_inline_boxes,
);
let lines_at_mid = inline_layout.lines().count();
if lines_at_mid > target_lines
|| count_emergency_line_breaks(inline_layout) > initial_emergency_breaks
{
left = mid;
} else {
right = mid;
}
}
let balanced_width = right.ceil();
if (balanced_width - max_width).abs() < device_pixel_ratio {
custom_inline_boxes.clear();
break_lines(
inline_layout,
max_width,
max_height,
line_height_hint,
text_wrap_mode,
spans,
custom_inline_boxes,
);
false
} else {
custom_inline_boxes.clear();
break_lines(
inline_layout,
balanced_width,
max_height,
line_height_hint,
text_wrap_mode,
spans,
custom_inline_boxes,
);
true
}
}
pub(crate) fn make_pretty_text(
inline_layout: &mut InlineLayout,
options: RebreakOptions,
spans: &[ProcessedInlineSpan<'_, '_>],
custom_inline_boxes: &mut Vec<parley::PositionedInlineBox>,
) -> bool {
let RebreakOptions {
max_width,
max_height,
line_height_hint,
text_wrap_mode,
} = options;
let Some(last_line_width) = inline_layout
.lines()
.last()
.map(|line| line.runs().map(|run| run.advance()).sum::<f32>())
else {
return false;
};
if last_line_width >= max_width / 3.0 {
return false;
}
let original_lines = inline_layout.lines().count();
if original_lines <= 1 {
return false;
}
let adjusted_width = max_width * 0.9;
custom_inline_boxes.clear();
break_lines(
inline_layout,
adjusted_width,
None,
line_height_hint,
text_wrap_mode,
spans,
custom_inline_boxes,
);
let adjusted_lines = inline_layout.lines().count();
let max_acceptable_lines = ((original_lines as f32) * 1.3).ceil() as usize;
if adjusted_lines <= max_acceptable_lines {
true
} else {
custom_inline_boxes.clear();
break_lines(
inline_layout,
max_width,
max_height,
line_height_hint,
text_wrap_mode,
spans,
custom_inline_boxes,
);
false
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_white_space_preserve() {
let input = " a \t b\n";
let mut previous_collapsible_space = false;
let mut previous_was_line_break = false;
let out = apply_white_space_collapse(
input,
WhiteSpaceCollapse::Preserve,
&mut previous_collapsible_space,
&mut previous_was_line_break,
);
assert_eq!(out, input);
}
#[test]
fn test_white_space_collapse() {
let input = " a \n\t b c\n\n ";
let mut previous_collapsible_space = false;
let mut previous_was_line_break = false;
let out = apply_white_space_collapse(
input,
WhiteSpaceCollapse::Collapse,
&mut previous_collapsible_space,
&mut previous_was_line_break,
);
assert_eq!(out, " a b c ");
}
#[test]
fn test_white_space_preserve_spaces() {
let input = "a \n b";
let mut previous_collapsible_space = false;
let mut previous_was_line_break = false;
let out = apply_white_space_collapse(
input,
WhiteSpaceCollapse::PreserveSpaces,
&mut previous_collapsible_space,
&mut previous_was_line_break,
);
assert_eq!(out, "a b");
}
#[test]
fn test_white_space_preserve_breaks() {
let input = "a \n b\tc";
let mut previous_collapsible_space = false;
let mut previous_was_line_break = false;
let out = apply_white_space_collapse(
input,
WhiteSpaceCollapse::PreserveBreaks,
&mut previous_collapsible_space,
&mut previous_was_line_break,
);
assert_eq!(out, "a \nb c");
}
#[test]
fn test_white_space_collapse_preserves_boundary_space_across_spans() {
let mut previous_collapsible_space = false;
let mut previous_was_line_break = false;
let left = apply_white_space_collapse(
"A",
WhiteSpaceCollapse::Collapse,
&mut previous_collapsible_space,
&mut previous_was_line_break,
);
let middle = apply_white_space_collapse(
" ",
WhiteSpaceCollapse::Collapse,
&mut previous_collapsible_space,
&mut previous_was_line_break,
);
let right = apply_white_space_collapse(
"B",
WhiteSpaceCollapse::Collapse,
&mut previous_collapsible_space,
&mut previous_was_line_break,
);
assert_eq!(format!("{left}{middle}{right}"), "A B");
}
#[test]
fn test_white_space_collapse_merges_adjacent_span_spaces() {
let mut previous_collapsible_space = false;
let mut previous_was_line_break = false;
let left = apply_white_space_collapse(
"A ",
WhiteSpaceCollapse::Collapse,
&mut previous_collapsible_space,
&mut previous_was_line_break,
);
let right = apply_white_space_collapse(
" B",
WhiteSpaceCollapse::Collapse,
&mut previous_collapsible_space,
&mut previous_was_line_break,
);
assert_eq!(format!("{left}{right}"), "A B");
}
#[test]
fn test_white_space_preserve_breaks_strips_spaces_after_span_boundary_line_break() {
let mut previous_collapsible_space = false;
let mut previous_was_line_break = false;
let left = apply_white_space_collapse(
"A\n",
WhiteSpaceCollapse::PreserveBreaks,
&mut previous_collapsible_space,
&mut previous_was_line_break,
);
let right = apply_white_space_collapse(
" B",
WhiteSpaceCollapse::PreserveBreaks,
&mut previous_collapsible_space,
&mut previous_was_line_break,
);
assert_eq!(format!("{left}{right}"), "A\nB");
}
}