use std::ops::{Bound, Range, RangeBounds};
use crate::kurbo::{Rect, Size};
use crate::{Color, Error, FontFamily, FontStyle, FontWeight, LineMetric, TextAttribute};
use unic_bidi::bidi_class::{BidiClass, BidiClassCategory};
pub const DEFAULT_FONT_SIZE: f64 = 12.0;
pub const DEFAULT_TEXT_COLOR: Color = Color::BLACK;
pub fn count_utf16(s: &str) -> usize {
let mut utf16_count = 0;
for &b in s.as_bytes() {
if (b as i8) >= -0x40 {
utf16_count += 1;
}
if b >= 0xf0 {
utf16_count += 1;
}
}
utf16_count
}
#[allow(clippy::explicit_counter_loop)]
pub fn count_until_utf16(s: &str, utf16_text_position: usize) -> Option<usize> {
let mut utf8_count = 0;
let mut utf16_count = 0;
for &b in s.as_bytes() {
if (b as i8) >= -0x40 {
utf16_count += 1;
}
if b >= 0xf0 {
utf16_count += 1;
}
if utf16_count > utf16_text_position {
return Some(utf8_count);
}
utf8_count += 1;
}
None
}
pub fn trailing_nlf(s: &str) -> Option<usize> {
if s.as_bytes().last() == Some(&b'\n') {
Some(1)
} else {
None
}
}
pub fn line_number_for_position(lines: &[LineMetric], position: usize) -> usize {
assert!(!lines.is_empty());
match lines.binary_search_by_key(&position, |item| item.start_offset) {
Ok(idx) => idx,
Err(idx) => idx - 1,
}
}
pub fn resolve_range(range: impl RangeBounds<usize>, len: usize) -> Range<usize> {
let start = match range.start_bound() {
Bound::Unbounded => 0,
Bound::Included(n) => *n,
Bound::Excluded(n) => *n + 1,
};
let end = match range.end_bound() {
Bound::Unbounded => len,
Bound::Included(n) => *n + 1,
Bound::Excluded(n) => *n,
};
start.min(len)..end.min(len)
}
const BLUR_EXTENT: f64 = 2.5;
pub fn size_for_blurred_rect(rect: Rect, radius: f64) -> Size {
let padding = BLUR_EXTENT * radius;
let rect_padded = rect.inflate(padding, padding);
let rect_exp = rect_padded.expand();
rect_exp.size()
}
pub fn compute_blurred_rect(rect: Rect, radius: f64, stride: usize, buf: &mut [u8]) -> Rect {
let radius_recip = radius.recip();
let xmax = rect.width() * radius_recip;
let ymax = rect.height() * radius_recip;
let padding = BLUR_EXTENT * radius;
let rect_padded = rect.inflate(padding, padding);
let rect_exp = rect_padded.expand();
let xfrac = rect_padded.x0 - rect_exp.x0;
let yfrac = rect_padded.y0 - rect_exp.y0;
let width = rect_exp.width() as usize;
let height = rect_exp.height() as usize;
let strip = (0..width)
.map(|i| {
let x = ((i as f64) - (xfrac + padding)) * radius_recip;
(255.0 * 0.25) * (compute_erf7(x) + compute_erf7(xmax - x))
})
.collect::<Vec<_>>();
{
for j in 0..height {
let y = ((j as f64) - (yfrac + padding)) * radius_recip;
let z = compute_erf7(y) + compute_erf7(ymax - y);
for i in 0..width {
buf[j * stride + i] = (z * strip[i]).round() as u8;
}
}
}
rect_exp
}
fn compute_erf7(x: f64) -> f64 {
let x = x * std::f64::consts::FRAC_2_SQRT_PI;
let xx = x * x;
let x = x + (0.24295 + (0.03395 + 0.0104 * xx) * xx) * (x * xx);
x / (1.0 + x * x).sqrt()
}
#[non_exhaustive]
#[allow(missing_docs)]
pub struct LayoutDefaults {
pub font: FontFamily,
pub font_size: f64,
pub weight: FontWeight,
pub fg_color: Color,
pub style: FontStyle,
pub underline: bool,
pub strikethrough: bool,
}
impl LayoutDefaults {
pub fn set(&mut self, val: impl Into<TextAttribute>) {
match val.into() {
TextAttribute::FontFamily(t) => self.font = t,
TextAttribute::FontSize(size) => {
self.font_size = if size <= 0.0 { DEFAULT_FONT_SIZE } else { size }
}
TextAttribute::Weight(weight) => self.weight = weight,
TextAttribute::Style(style) => self.style = style,
TextAttribute::Underline(flag) => self.underline = flag,
TextAttribute::TextColor(color) => self.fg_color = color,
TextAttribute::Strikethrough(flag) => self.strikethrough = flag,
}
}
}
impl Default for LayoutDefaults {
fn default() -> Self {
LayoutDefaults {
font: FontFamily::default(),
font_size: DEFAULT_FONT_SIZE,
weight: FontWeight::default(),
fg_color: DEFAULT_TEXT_COLOR,
style: FontStyle::default(),
underline: false,
strikethrough: false,
}
}
}
pub fn unpremul(x: u8, a: u8) -> u8 {
if a == 0 {
0
} else {
let y = (x as u32 * 255 + (a as u32 / 2)) / (a as u32);
y.min(255) as u8
}
}
pub fn unpremultiply_rgba(data: &mut [u8]) {
for i in (0..data.len()).step_by(4) {
let a = data[i + 3];
if a != 0 {
for x in &mut data[i..(i + 3)] {
*x = unpremul(*x, a);
}
}
}
}
pub fn first_strong_rtl(text: &str) -> bool {
text.chars()
.take(200)
.map(BidiClass::of)
.find(|c| c.category() == BidiClassCategory::Strong)
.map(|c| c.is_rtl())
.unwrap_or(false)
}
pub fn expected_image_buffer_size(row_size: usize, height: usize, stride: usize) -> usize {
if height == 0 {
return 0;
}
stride * (height - 1) + row_size
}
pub fn image_buffer_to_tightly_packed(
buff: &[u8],
width: usize,
height: usize,
stride: usize,
format: crate::ImageFormat,
) -> Result<Vec<u8>, Error> {
if width == 0 || height == 0 {
return Ok(vec![]);
}
let bytes_per_pixel = format.bytes_per_pixel();
let row_size = width * bytes_per_pixel;
if buff.len() < expected_image_buffer_size(row_size, height, stride) {
return Err(Error::InvalidInput);
}
if stride == row_size {
return Ok(buff.to_vec());
}
let mut new_buff = vec![0u8; height * row_size];
for y in 0..height {
let src_row_start = y * stride;
let dst_row_start = y * row_size;
let src_row_slice = &buff[src_row_start..(src_row_start + row_size)];
let dst_row_slice = &mut new_buff[dst_row_start..(dst_row_start + row_size)];
dst_row_slice.copy_from_slice(src_row_slice);
}
Ok(new_buff)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_count_until_utf16() {
let input = "é\u{0023}\u{FE0F}\u{20E3}1";
assert_eq!(count_until_utf16(input, 0), Some(0));
assert_eq!(count_until_utf16(input, 1), Some(2));
assert_eq!(count_until_utf16(input, 2), Some(3));
assert_eq!(count_until_utf16(input, 3), Some(6));
assert_eq!(count_until_utf16(input, 4), Some(9));
assert_eq!(count_until_utf16(input, 5), None);
assert_eq!(count_until_utf16("", 0), None);
}
#[test]
fn test_image_buffer_to_tightly_packed() {
let w: u16 = 7;
let h: u16 = 11;
let format = crate::ImageFormat::RgbaSeparate;
let bpp = format.bytes_per_pixel();
let make_buffer_with_stride = |stride: usize| -> Vec<u8> {
let mut buff = vec![0u8; stride * h as usize];
let split_u16 = |x: u16| -> (u8, u8) {
let most_significant = (x >> 8) as u8;
let least_significant = (x & 0xff) as u8;
(most_significant, least_significant)
};
for y in 0..h {
for x in 0..w {
let i: usize = y as usize * stride + x as usize * bpp;
let (r, g) = split_u16(x);
let (b, a) = split_u16(y);
buff[i] = r;
buff[i + 1] = g;
buff[i + 2] = b;
buff[i + 3] = a;
}
}
buff
};
let tightly_packed_buffer = make_buffer_with_stride(w as usize * bpp);
let result = image_buffer_to_tightly_packed(
&tightly_packed_buffer,
w as usize,
h as usize,
w as usize * bpp,
format,
);
assert!(result.is_ok());
let result = result.unwrap();
assert_eq!(result, tightly_packed_buffer);
let buff = make_buffer_with_stride(w as usize * bpp + 1);
let result = image_buffer_to_tightly_packed(
&buff,
w as usize,
h as usize,
w as usize * bpp + 1,
format,
);
assert!(result.is_ok());
let result = result.unwrap();
assert_eq!(result, tightly_packed_buffer);
let small_buff = tightly_packed_buffer[..(tightly_packed_buffer.len() - 1)].to_vec();
let result = image_buffer_to_tightly_packed(
&small_buff,
w as usize,
h as usize,
w as usize * bpp,
format,
);
assert!(result.is_err());
let result = result.unwrap_err();
assert_eq!(result.to_string(), Error::InvalidInput.to_string());
}
}