use crate::quickdraw::fonts::{
get_font_face_or_default, get_italic_glyph as get_italic_glyph_fn, get_macroman_glyph,
override_format, FontMetrics, Glyph,
};
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub(crate) struct QuickDrawTextStyle(u8);
impl QuickDrawTextStyle {
pub(crate) const BOLD_BIT: u8 = 0x01;
pub(crate) const ITALIC_BIT: u8 = 0x02;
pub(crate) const UNDERLINE_BIT: u8 = 0x04;
pub(crate) const OUTLINE_BIT: u8 = 0x08;
pub(crate) const SHADOW_BIT: u8 = 0x10;
pub(crate) const CONDENSE_BIT: u8 = 0x20;
pub(crate) const EXTEND_BIT: u8 = 0x40;
const EFFECT_BITS: u8 = Self::BOLD_BIT
| Self::ITALIC_BIT
| Self::UNDERLINE_BIT
| Self::OUTLINE_BIT
| Self::SHADOW_BIT
| Self::CONDENSE_BIT
| Self::EXTEND_BIT;
const PER_GLYPH_EFFECT_BITS: u8 = Self::EFFECT_BITS & !Self::UNDERLINE_BIT;
pub(crate) const fn from_bits(bits: u8) -> Self {
Self(bits & Self::EFFECT_BITS)
}
pub(crate) const fn plain() -> Self {
Self(0)
}
pub(crate) const fn is_plain(self) -> bool {
self.0 == 0
}
pub(crate) const fn has_per_glyph_effect(self) -> bool {
self.0 & Self::PER_GLYPH_EFFECT_BITS != 0
}
pub(crate) const fn bold(self) -> bool {
self.0 & Self::BOLD_BIT != 0
}
pub(crate) const fn italic(self) -> bool {
self.0 & Self::ITALIC_BIT != 0
}
pub(crate) const fn underline(self) -> bool {
self.0 & Self::UNDERLINE_BIT != 0
}
pub(crate) const fn outline(self) -> bool {
self.0 & Self::OUTLINE_BIT != 0
}
pub(crate) const fn shadow(self) -> bool {
self.0 & Self::SHADOW_BIT != 0
}
pub(crate) const fn condensed(self) -> bool {
self.0 & Self::CONDENSE_BIT != 0
}
pub(crate) const fn extended(self) -> bool {
self.0 & Self::EXTEND_BIT != 0
}
pub(crate) fn glyph_advance(self, glyph_advance: i32) -> i32 {
let mut advance = glyph_advance;
if self.bold() {
advance += 1;
}
if self.outline() {
advance += 1;
}
if self.shadow() {
advance += 2;
}
if self.condensed() && advance >= 6 {
advance -= 1;
}
if self.extended() {
advance += 1;
}
advance.max(1)
}
pub(crate) const fn glyph_y_offset(self) -> i32 {
if self.shadow() {
-1
} else {
0
}
}
pub(crate) const fn smear_max(self) -> Option<i32> {
if self.shadow() && self.outline() {
Some(3)
} else if self.shadow() {
Some(2)
} else if self.outline() {
Some(1)
} else {
None
}
}
}
pub fn get_font_metrics(font_id: i16, size: i16) -> FontMetrics {
get_font_face_or_default(font_id, size).metrics
}
pub fn get_glyph(font_id: i16, size: i16, ch: char) -> Option<(&'static Glyph, &'static [u8])> {
let face = get_font_face_or_default(font_id, size);
let glyphs = face.glyphs;
let data = face.data;
if (' '..='~').contains(&ch) {
let idx = (ch as usize) - 32;
if idx < glyphs.len() {
let glyph = &glyphs[idx];
if glyph.width != 0 || glyph.height != 0 || glyph.advance != 0 {
return Some((glyph, data));
}
}
return None;
}
let mac_code = ch as u32;
if (0x80..=0xFF).contains(&mac_code) {
return macroman_or_ascii_fallback(font_id, size, mac_code as u8);
}
if ch == '\u{2318}' {
if let Some(hit) =
override_symbol_glyph(font_id, size, override_format::COMMAND_SYMBOL_GLYPH_INDEX)
{
return Some(hit);
}
if let Some(hit) = crate::quickdraw::fonts::pixel_font::menu_symbols::get_glyph(ch) {
return Some(hit);
}
return get_macroman_glyph(font_id, size, 0x11);
}
if ch == '\u{2713}' {
if let Some(hit) =
override_symbol_glyph(font_id, size, override_format::CHECKMARK_SYMBOL_GLYPH_INDEX)
{
return Some(hit);
}
if let Some(hit) = crate::quickdraw::fonts::pixel_font::menu_symbols::get_glyph(ch) {
return Some(hit);
}
return get_macroman_glyph(font_id, size, 0x12);
}
if ch == '\u{14}' || ch == '\u{F8FF}' {
if let Some(hit) =
override_symbol_glyph(font_id, size, override_format::APPLE_SYMBOL_GLYPH_INDEX)
{
return Some(hit);
}
return get_macroman_glyph(font_id, size, 0x14);
}
if let Some(mac_code @ 0x80..=0xFF) = crate::mac_roman::encode_mac_roman_char(ch) {
return macroman_or_ascii_fallback(font_id, size, mac_code);
}
None
}
fn override_symbol_glyph(
font_id: i16,
size: i16,
index: usize,
) -> Option<(&'static Glyph, &'static [u8])> {
let face = get_font_face_or_default(font_id, size);
let glyph = face.glyphs.get(index)?;
if glyph.width == 0 && glyph.height == 0 && glyph.advance == 0 {
return None;
}
Some((glyph, face.data))
}
fn macroman_or_ascii_fallback(
font_id: i16,
size: i16,
mac_code: u8,
) -> Option<(&'static Glyph, &'static [u8])> {
if let Some(hit) = get_macroman_glyph(font_id, size, mac_code) {
return Some(hit);
}
if mac_code == 0xAA {
return crate::quickdraw::fonts::pixel_font::menu_symbols::get_glyph('\u{2122}');
}
let ascii_fallback: char = match mac_code {
0xD0 | 0xD1 => '-', 0xD2 | 0xD3 => '"', 0xD4 | 0xD5 => '\'', 0xA5 => '*', 0xCA => ' ', 0xE1 | 0xE5 => '.', _ => return None,
};
get_glyph(font_id, size, ascii_fallback)
}
pub fn get_glyph_italic(
font_id: i16,
size: i16,
ch: char,
) -> Option<(&'static Glyph, &'static [u8])> {
get_italic_glyph_fn(font_id, size, ch)
}
pub fn get_underline_thickness(_font_id: i16, _size: i16) -> i16 {
1
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct WrappedTextLine {
pub(crate) start: usize,
pub(crate) visible_end: usize,
pub(crate) next: usize,
}
pub(crate) fn wrap_classic_text<F>(
text: &[u8],
max_width: i16,
mut byte_advance: F,
) -> Vec<WrappedTextLine>
where
F: FnMut(usize, u8) -> i16,
{
let mut lines = Vec::new();
let mut line_start = 0usize;
let max_width = max_width.max(1);
while line_start < text.len() {
let mut index = line_start;
let mut width = 0i16;
let mut last_whitespace = None::<(usize, usize)>;
let mut completed = false;
while index < text.len() {
if matches!(text[index], b'\r' | b'\n') {
let next = if text[index] == b'\r' && text.get(index + 1) == Some(&b'\n') {
index + 2
} else {
index + 1
};
lines.push(WrappedTextLine {
start: line_start,
visible_end: trim_classic_line_end(text, line_start, index),
next,
});
line_start = next;
completed = true;
break;
}
let advance = byte_advance(index, text[index]).max(0);
if index > line_start && width.saturating_add(advance) > max_width {
let (visible_end, next) = if text[index] <= b' ' {
let mut next = index + 1;
while next < text.len()
&& text[next] <= b' '
&& !matches!(text[next], b'\r' | b'\n')
{
next += 1;
}
(trim_classic_line_end(text, line_start, index), next)
} else if let Some((visible_end, next)) = last_whitespace {
(visible_end, next)
} else {
(index, index)
};
lines.push(WrappedTextLine {
start: line_start,
visible_end,
next,
});
line_start = next;
completed = true;
break;
}
width = width.saturating_add(advance);
if text[index] <= b' ' {
let whitespace_start = index;
let mut next = index + 1;
while next < text.len()
&& text[next] <= b' '
&& !matches!(text[next], b'\r' | b'\n')
{
next += 1;
}
last_whitespace = Some((whitespace_start, next));
}
index += 1;
}
if !completed {
lines.push(WrappedTextLine {
start: line_start,
visible_end: trim_classic_line_end(text, line_start, text.len()),
next: text.len(),
});
break;
}
}
lines
}
fn trim_classic_line_end(text: &[u8], start: usize, mut end: usize) -> usize {
while end > start && text[end - 1] <= b' ' {
end -= 1;
}
end
}
#[cfg(test)]
mod tests {
use super::{get_glyph, wrap_classic_text, QuickDrawTextStyle, WrappedTextLine};
#[test]
fn quickdraw_style_plan_combines_all_low_order_face_bits() {
let style = QuickDrawTextStyle::from_bits(0xff);
assert!(style.bold());
assert!(style.italic());
assert!(style.underline());
assert!(style.outline());
assert!(style.shadow());
assert!(style.condensed());
assert!(style.extended());
assert_eq!(style.glyph_y_offset(), -1);
assert_eq!(style.smear_max(), Some(3));
assert_eq!(style.glyph_advance(6), 10);
assert!(QuickDrawTextStyle::from_bits(0x80).is_plain());
}
#[test]
fn built_in_system_font_renders_menu_symbols() {
for (symbol, name) in [('\u{2318}', "Command"), ('\u{2713}', "checkmark")] {
let (glyph, data) = get_glyph(0, 12, symbol)
.unwrap_or_else(|| panic!("{name} symbol should resolve to a bitmap glyph"));
let glyph_len = usize::from(glyph.width) * usize::from(glyph.height);
assert!(
data[glyph.data_offset..glyph.data_offset + glyph_len]
.iter()
.any(|pixel| *pixel != 0),
"{name} symbol should contain visible pixels"
);
}
}
#[test]
fn unicode_hle_text_uses_mac_roman_extended_glyphs() {
let (glyph, data) = get_glyph(0, 12, '™').expect("Mac Roman trademark glyph");
let glyph_len = usize::from(glyph.width) * usize::from(glyph.height);
assert!(data[glyph.data_offset..glyph.data_offset + glyph_len]
.iter()
.any(|pixel| *pixel != 0));
}
#[test]
fn classic_text_wrap_prefers_words_and_hides_wrap_whitespace() {
let text = b"one two three";
assert_eq!(
wrap_classic_text(text, 6, |_, _| 1),
vec![
WrappedTextLine {
start: 0,
visible_end: 3,
next: 4,
},
WrappedTextLine {
start: 4,
visible_end: 7,
next: 8,
},
WrappedTextLine {
start: 8,
visible_end: 13,
next: 13,
},
]
);
}
#[test]
fn classic_text_wrap_normalizes_cr_lf_and_crlf_hard_breaks() {
let text = b"one\rtwo\nthree\r\nfour";
assert_eq!(
wrap_classic_text(text, 80, |_, _| 1),
vec![
WrappedTextLine {
start: 0,
visible_end: 3,
next: 4,
},
WrappedTextLine {
start: 4,
visible_end: 7,
next: 8,
},
WrappedTextLine {
start: 8,
visible_end: 13,
next: 15,
},
WrappedTextLine {
start: 15,
visible_end: 19,
next: 19,
},
]
);
}
#[test]
fn classic_text_wrap_breaks_an_overlong_word_at_a_character() {
assert_eq!(
wrap_classic_text(b"toolbox", 3, |_, _| 1),
vec![
WrappedTextLine {
start: 0,
visible_end: 3,
next: 3,
},
WrappedTextLine {
start: 3,
visible_end: 6,
next: 6,
},
WrappedTextLine {
start: 6,
visible_end: 7,
next: 7,
},
]
);
}
}