use std::borrow::Cow;
use std::fs;
use std::io::{self};
pub struct CharWidthMeasurer {
ranges: Cow<'static, [(u32, u32, f64)]>,
ascii: [f64; 128],
pub em_width: f64,
}
impl CharWidthMeasurer {
pub fn is_control_char(char_code: u32) -> bool {
char_code <= 31 || char_code == 127
}
fn build(ranges: Cow<'static, [(u32, u32, f64)]>) -> Self {
let mut measurer = CharWidthMeasurer {
ranges,
ascii: [f64::NAN; 128],
em_width: 0.0,
};
for code in 0..128u32 {
if let Some(width) = measurer.lookup_range(code) {
measurer.ascii[code as usize] = width;
}
}
measurer.em_width = measurer.width_of("m", true);
measurer
}
fn lookup_range(&self, char_code: u32) -> Option<f64> {
let idx = self
.ranges
.partition_point(|&(lower, _, _)| lower <= char_code);
let &(_, upper, width) = self.ranges.get(idx.checked_sub(1)?)?;
(char_code <= upper).then_some(width)
}
pub fn from_data(data: Vec<(u32, u32, f64)>) -> Self {
let mut ranges: Vec<(u32, u32, f64)> = Vec::with_capacity(data.len());
for new in data {
Self::overwrite(&mut ranges, new);
}
Self::build(Cow::Owned(ranges))
}
pub fn from_sorted_static(ranges: &'static [(u32, u32, f64)]) -> Self {
debug_assert!(ranges.windows(2).all(|w| w[0].1 < w[1].0));
Self::build(Cow::Borrowed(ranges))
}
fn overwrite(ranges: &mut Vec<(u32, u32, f64)>, new: (u32, u32, f64)) {
let (new_lower, new_upper, _) = new;
if new_lower > new_upper {
return;
}
let start = ranges.partition_point(|&(_, upper, _)| upper < new_lower);
let end = ranges.partition_point(|&(lower, _, _)| lower <= new_upper);
let mut replacement = Vec::with_capacity(3);
if start < end {
let (first_lower, _, first_width) = ranges[start];
if first_lower < new_lower {
replacement.push((first_lower, new_lower - 1, first_width));
}
}
replacement.push(new);
if start < end {
let (_, last_upper, last_width) = ranges[end - 1];
if last_upper > new_upper {
replacement.push((new_upper + 1, last_upper, last_width));
}
}
ranges.splice(start..end, replacement);
}
fn parse_json(data: &str) -> io::Result<Vec<(u32, u32, f64)>> {
serde_json::from_str(data)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))
}
pub fn load_sync(path: &str) -> io::Result<Self> {
let json_str = fs::read_to_string(path)?;
Ok(Self::from_data(Self::parse_json(&json_str)?))
}
pub fn load_from_str(data: &str) -> io::Result<Self> {
Ok(Self::from_data(Self::parse_json(data)?))
}
pub fn width_of_char_code(&self, char_code: u32) -> Option<f64> {
if Self::is_control_char(char_code) {
return Some(0.0);
}
if char_code < 128 {
let width = self.ascii[char_code as usize];
return if width.is_nan() { None } else { Some(width) };
}
self.lookup_range(char_code)
}
pub fn width_of(&self, text: &str, guess: bool) -> f64 {
let mut total = 0.0;
for ch in text.chars() {
let code = ch as u32;
match self.width_of_char_code(code) {
Some(width) => total += width,
None => {
if guess {
total += self.em_width;
} else {
panic!("No width available for character code {code} ({ch:?})");
}
}
}
}
total
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_control_chars() {
assert!(CharWidthMeasurer::is_control_char(0));
assert!(CharWidthMeasurer::is_control_char(31));
assert!(CharWidthMeasurer::is_control_char(127));
assert!(!CharWidthMeasurer::is_control_char(32));
assert!(!CharWidthMeasurer::is_control_char(128));
}
#[test]
fn test_from_data() {
let data = vec![(65, 90, 10.0), (97, 122, 8.0)]; let measurer = CharWidthMeasurer::from_data(data);
assert_eq!(measurer.width_of_char_code(65), Some(10.0)); assert_eq!(measurer.width_of_char_code(90), Some(10.0)); assert_eq!(measurer.width_of_char_code(97), Some(8.0)); assert_eq!(measurer.width_of_char_code(122), Some(8.0)); assert_eq!(measurer.width_of_char_code(64), None); }
#[test]
fn test_from_unsorted_data() {
let data = vec![(97, 122, 8.0), (65, 90, 10.0)];
let measurer = CharWidthMeasurer::from_data(data);
assert_eq!(measurer.width_of_char_code(65), Some(10.0));
assert_eq!(measurer.width_of_char_code(97), Some(8.0));
assert_eq!(measurer.width_of_char_code(123), None);
}
#[test]
fn test_overlapping_ranges_later_wins() {
let data = vec![(97, 122, 8.0), (109, 109, 16.0)];
let measurer = CharWidthMeasurer::from_data(data);
assert_eq!(measurer.width_of_char_code(109), Some(16.0)); assert_eq!(measurer.width_of_char_code(108), Some(8.0)); assert_eq!(measurer.width_of_char_code(110), Some(8.0)); }
#[test]
fn test_from_sorted_static() {
static RANGES: [(u32, u32, f64); 2] = [(65, 90, 10.0), (109, 109, 16.0)];
let measurer = CharWidthMeasurer::from_sorted_static(&RANGES);
assert_eq!(measurer.em_width, 16.0);
assert_eq!(measurer.width_of_char_code(70), Some(10.0));
assert_eq!(measurer.width_of_char_code(91), None);
}
#[test]
fn test_width_of() {
let data = vec![
(65, 90, 10.0), (97, 122, 8.0), (109, 109, 16.0), ];
let measurer = CharWidthMeasurer::from_data(data);
assert_eq!(measurer.em_width, 16.0);
assert_eq!(measurer.width_of("ABC", true), 30.0);
assert_eq!(measurer.width_of("abc", true), 24.0);
assert_eq!(measurer.width_of("Am", true), 26.0);
}
#[test]
#[should_panic(expected = "No width available for character code")]
fn test_width_of_no_guess() {
let data = vec![(65, 90, 10.0)];
let measurer = CharWidthMeasurer::from_data(data);
measurer.width_of("A測", false); }
}