use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::rc::{Rc, Weak};
use windows::core::{
Error as WinError, IUnknown, Interface, Result as WinResult, BOOL, HSTRING, PCWSTR,
};
use windows::Win32::Foundation::E_UNEXPECTED;
use windows::Win32::Graphics::Direct2D::Common::{D2D1_COLOR_F, D2D_RECT_F};
use windows::Win32::Graphics::Direct2D::{
ID2D1RenderTarget, ID2D1SolidColorBrush, D2D1_ANTIALIAS_MODE_PER_PRIMITIVE,
D2D1_DRAW_TEXT_OPTIONS_CLIP, D2D1_ROUNDED_RECT,
};
use windows::Win32::Graphics::DirectWrite::{
DWriteCreateFactory, IDWriteFactory, IDWriteFactory2, IDWriteFactory5, IDWriteFontCollection,
IDWriteFontCollection1, IDWriteFontFallback, IDWriteFontFile, IDWriteLocalizedStrings,
IDWriteTextFormat, IDWriteTextFormat1, DWRITE_FACTORY_TYPE_SHARED, DWRITE_FONT_METRICS,
DWRITE_FONT_STRETCH_NORMAL, DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_WEIGHT,
DWRITE_FONT_WEIGHT_BOLD, DWRITE_FONT_WEIGHT_NORMAL, DWRITE_MEASURING_MODE_NATURAL,
DWRITE_UNICODE_RANGE, DWRITE_WORD_WRAPPING_NO_WRAP,
};
use windows_numerics::Vector2;
use crate::event::Rect;
use crate::win::msg::pt_to_dip;
use crate::Color;
pub struct DWrite {
factory: IDWriteFactory,
text_format: IDWriteTextFormat,
bold_text_format: IDWriteTextFormat,
family: String,
fallback: Option<IDWriteFontFallback>,
minimap_formats: RefCell<HashMap<i32, IDWriteTextFormat>>,
natural_line_height: f32,
}
impl DWrite {
pub fn new(
family: &str,
size_pt: f32,
fallback: Option<&IDWriteFontFallback>,
) -> WinResult<(Self, f32, f32)> {
let factory: IDWriteFactory = unsafe { DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED)? };
let size_dip = pt_to_dip(size_pt);
let text_format =
create_text_format(&factory, family, size_dip, DWRITE_FONT_WEIGHT_NORMAL)?;
let bold_text_format =
create_text_format(&factory, family, size_dip, DWRITE_FONT_WEIGHT_BOLD)?;
unsafe { text_format.SetWordWrapping(DWRITE_WORD_WRAPPING_NO_WRAP)? };
unsafe { bold_text_format.SetWordWrapping(DWRITE_WORD_WRAPPING_NO_WRAP)? };
if let Some(fallback) = fallback {
if let Err(err) = apply_fallback_to_format(&text_format, fallback) {
crate::diagnostics::emit(format!(
"quadraui: IDWriteTextFormat1::SetFontFallback failed for the regular text \
format ({err:?}); continuing without a Nerd-Font fallback"
));
}
if let Err(err) = apply_fallback_to_format(&bold_text_format, fallback) {
crate::diagnostics::emit(format!(
"quadraui: IDWriteTextFormat1::SetFontFallback failed for the bold text \
format ({err:?}); continuing without a Nerd-Font fallback"
));
}
}
let font_metrics = font_face_metrics(&factory, family)?;
let units_per_em = (font_metrics.designUnitsPerEm as f32).max(1.0);
let line_gap = (font_metrics.lineGap as f32).max(0.0);
let line_height = (font_metrics.ascent as f32 + font_metrics.descent as f32 + line_gap)
/ units_per_em
* size_dip;
let (char_width, _) = measure_text(&factory, &text_format, "0")?;
Ok((
Self {
factory,
text_format,
bold_text_format,
family: family.to_string(),
fallback: fallback.cloned(),
minimap_formats: RefCell::new(HashMap::new()),
natural_line_height: line_height,
},
line_height,
char_width,
))
}
pub(crate) fn natural_line_height(&self) -> f32 {
self.natural_line_height
}
pub(crate) fn with_size(&self, size_pt: f32) -> WinResult<DWrite> {
DWrite::new(&self.family, size_pt, self.fallback.as_ref()).map(|(dw, _, _)| dw)
}
pub fn measure_text(&self, text: &str) -> WinResult<(f32, f32)> {
measure_text(&self.factory, &self.text_format, text)
}
pub fn measure_text_styled(&self, text: &str, bold: bool) -> WinResult<(f32, f32)> {
measure_text(&self.factory, self.format_for(bold), text)
}
pub fn draw_text(
&self,
target: &ID2D1RenderTarget,
text: &str,
rect: Rect,
color: Color,
) -> WinResult<()> {
draw_text(target, &self.text_format, text, rect, color)
}
pub fn draw_text_styled(
&self,
target: &ID2D1RenderTarget,
text: &str,
rect: Rect,
color: Color,
bold: bool,
) -> WinResult<()> {
draw_text(target, self.format_for(bold), text, rect, color)
}
fn format_for(&self, bold: bool) -> &IDWriteTextFormat {
if bold {
&self.bold_text_format
} else {
&self.text_format
}
}
pub(crate) fn minimap_text_format(&self, size_px: f64) -> WinResult<IDWriteTextFormat> {
let key = (size_px.round() as i32).max(1);
if let Some(format) = self.minimap_formats.borrow().get(&key) {
return Ok(format.clone());
}
let format = create_text_format(
&self.factory,
&self.family,
key as f32,
DWRITE_FONT_WEIGHT_NORMAL,
)?;
unsafe { format.SetWordWrapping(DWRITE_WORD_WRAPPING_NO_WRAP)? };
if let Some(fallback) = &self.fallback {
if let Err(err) = apply_fallback_to_format(&format, fallback) {
crate::diagnostics::emit(format!(
"quadraui: IDWriteTextFormat1::SetFontFallback failed for a minimap text \
format at {key}px ({err:?}); continuing without a Nerd-Font fallback"
));
}
}
self.minimap_formats
.borrow_mut()
.insert(key, format.clone());
Ok(format)
}
pub(crate) fn draw_text_minimap(
&self,
target: &ID2D1RenderTarget,
text: &str,
rect: Rect,
color: Color,
size_px: f64,
) -> WinResult<()> {
let format = self.minimap_text_format(size_px)?;
draw_text(target, &format, text, rect, color)
}
#[cfg(test)]
pub(crate) fn minimap_format_cache_len(&self) -> usize {
self.minimap_formats.borrow().len()
}
#[cfg(test)]
pub(crate) fn has_cached_minimap_px(&self, size_px: f64) -> bool {
let key = (size_px.round() as i32).max(1);
self.minimap_formats.borrow().contains_key(&key)
}
}
impl crate::primitives::layout_metrics::TextMeasure for DWrite {
fn width_of(&self, text: &str) -> f32 {
self.measure_text(text).map(|(w, _)| w).unwrap_or(0.0)
}
}
fn create_text_format(
factory: &IDWriteFactory,
family: &str,
size_dip: f32,
weight: DWRITE_FONT_WEIGHT,
) -> WinResult<IDWriteTextFormat> {
unsafe {
factory.CreateTextFormat(
&HSTRING::from(family),
None,
weight,
DWRITE_FONT_STYLE_NORMAL,
DWRITE_FONT_STRETCH_NORMAL,
size_dip,
&HSTRING::from("en-us"),
)
}
}
fn apply_fallback_to_format(
format: &IDWriteTextFormat,
fallback: &IDWriteFontFallback,
) -> WinResult<()> {
let format1: IDWriteTextFormat1 = format.cast()?;
unsafe { format1.SetFontFallback(fallback) }
}
pub fn register_font_from_memory(bytes: &[u8]) -> WinResult<(IDWriteFontCollection1, Vec<String>)> {
let factory: IDWriteFactory5 = unsafe { DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED)? };
let loader = unsafe { factory.CreateInMemoryFontFileLoader()? };
unsafe { factory.RegisterFontFileLoader(&loader)? };
let factory_base: IDWriteFactory = factory.cast()?;
let file: IDWriteFontFile = unsafe {
loader.CreateInMemoryFontFileReference(
&factory_base,
bytes.as_ptr().cast(),
bytes.len() as u32,
None::<&IUnknown>,
)?
};
let builder = unsafe { factory.CreateFontSetBuilder()? };
unsafe { builder.AddFontFile(&file)? };
let font_set = unsafe { builder.CreateFontSet()? };
let collection = unsafe { factory.CreateFontCollectionFromFontSet(&font_set)? };
let count = unsafe { collection.GetFontFamilyCount() };
let mut names = Vec::with_capacity(count as usize);
for i in 0..count {
let family = unsafe { collection.GetFontFamily(i)? };
let localized = unsafe { family.GetFamilyNames()? };
names.push(read_localized_string(&localized, 0)?);
}
Ok((collection, names))
}
fn read_localized_string(strings: &IDWriteLocalizedStrings, index: u32) -> WinResult<String> {
let len = unsafe { strings.GetStringLength(index)? };
let mut buf = vec![0u16; len as usize + 1];
unsafe { strings.GetString(index, &mut buf)? };
Ok(String::from_utf16_lossy(&buf[..len as usize]))
}
pub fn build_nerd_font_fallback(
family: &str,
collection: Option<&IDWriteFontCollection1>,
) -> WinResult<IDWriteFontFallback> {
build_nerd_font_fallback_multi(&[(family, collection)])
}
pub fn build_nerd_font_fallback_multi(
mappings: &[(&str, Option<&IDWriteFontCollection1>)],
) -> WinResult<IDWriteFontFallback> {
let factory: IDWriteFactory2 = unsafe { DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED)? };
let system_fallback = unsafe { factory.GetSystemFontFallback()? };
let builder = unsafe { factory.CreateFontFallbackBuilder()? };
unsafe { builder.AddMappings(&system_fallback)? };
let ranges = [DWRITE_UNICODE_RANGE {
first: 0x0000_0000,
last: 0x0010_FFFF,
}];
for &(family, collection) in mappings {
let family_wide = HSTRING::from(family);
let family_ptrs = [family_wide.as_ptr()];
let base_collection: Option<IDWriteFontCollection> = match collection {
Some(c) => Some(c.cast()?),
None => None,
};
unsafe {
builder.AddMapping(
&ranges,
&family_ptrs,
base_collection.as_ref(),
&HSTRING::from("en-us"),
PCWSTR::null(),
1.0,
)?
};
}
unsafe { builder.CreateFontFallback() }
}
fn font_face_metrics(factory: &IDWriteFactory, family: &str) -> WinResult<DWRITE_FONT_METRICS> {
let mut collection: Option<IDWriteFontCollection> = None;
unsafe { factory.GetSystemFontCollection(&mut collection, false)? };
let collection = collection.ok_or_else(|| {
WinError::new(
E_UNEXPECTED,
"GetSystemFontCollection returned Ok(()) but left the collection unpopulated",
)
})?;
let mut index = 0u32;
let mut exists = BOOL(0);
unsafe { collection.FindFamilyName(&HSTRING::from(family), &mut index, &mut exists)? };
let index = if exists.as_bool() { index } else { 0 };
let font_family = unsafe { collection.GetFontFamily(index)? };
let font = unsafe {
font_family.GetFirstMatchingFont(
DWRITE_FONT_WEIGHT_NORMAL,
DWRITE_FONT_STRETCH_NORMAL,
DWRITE_FONT_STYLE_NORMAL,
)?
};
let face = unsafe { font.CreateFontFace()? };
let mut metrics = DWRITE_FONT_METRICS::default();
unsafe { face.GetMetrics(&mut metrics) };
Ok(metrics)
}
pub fn has_font_family(family: &str) -> WinResult<bool> {
let factory: IDWriteFactory = unsafe { DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED)? };
let mut collection: Option<IDWriteFontCollection> = None;
unsafe { factory.GetSystemFontCollection(&mut collection, false)? };
let collection = collection.ok_or_else(|| {
WinError::new(
E_UNEXPECTED,
"GetSystemFontCollection returned Ok(()) but left the collection unpopulated",
)
})?;
let mut index = 0u32;
let mut exists = BOOL(0);
unsafe { collection.FindFamilyName(&HSTRING::from(family), &mut index, &mut exists)? };
Ok(exists.as_bool())
}
fn measure_text(
factory: &IDWriteFactory,
format: &IDWriteTextFormat,
text: &str,
) -> WinResult<(f32, f32)> {
let wide: Vec<u16> = text.encode_utf16().collect();
let layout = unsafe { factory.CreateTextLayout(&wide, format, f32::MAX, f32::MAX)? };
let mut metrics = Default::default();
unsafe { layout.GetMetrics(&mut metrics)? };
Ok((metrics.width, metrics.height))
}
fn draw_text(
target: &ID2D1RenderTarget,
format: &IDWriteTextFormat,
text: &str,
rect: Rect,
color: Color,
) -> WinResult<()> {
if crate::testing::text_run_sink_active() {
let (tx, ty) = CURRENT_TRANSLATION.get();
let abs_rect = Rect::new(rect.x + tx, rect.y + ty, rect.width, rect.height);
crate::testing::record_text_run(text, abs_rect);
}
let wide: Vec<u16> = text.encode_utf16().collect();
let brush = get_or_create_brush(target, color)?;
let layout_rect = D2D_RECT_F {
left: rect.x,
top: rect.y,
right: rect.x + rect.width,
bottom: rect.y + rect.height,
};
unsafe {
target.DrawText(
&wide,
format,
&layout_rect,
&brush,
D2D1_DRAW_TEXT_OPTIONS_CLIP,
DWRITE_MEASURING_MODE_NATURAL,
);
}
Ok(())
}
pub(crate) fn color_to_d2d(color: Color) -> D2D1_COLOR_F {
D2D1_COLOR_F {
r: color.r as f32 / 255.0,
g: color.g as f32 / 255.0,
b: color.b as f32 / 255.0,
a: color.a as f32 / 255.0,
}
}
pub(crate) type BrushCache = Rc<BrushMap>;
type BrushMap = RefCell<HashMap<Color, ID2D1SolidColorBrush>>;
pub(crate) fn new_brush_cache() -> BrushCache {
Rc::new(RefCell::new(HashMap::new()))
}
thread_local! {
static ACTIVE_BRUSH_CACHE: RefCell<Option<(usize, Weak<BrushMap>)>> = const { RefCell::new(None) };
static BRUSH_CREATIONS: Cell<u32> = const { Cell::new(0) };
}
pub(crate) fn activate_brush_cache(target: &ID2D1RenderTarget, cache: &BrushCache) {
let entry = (target.as_raw() as usize, Rc::downgrade(cache));
ACTIVE_BRUSH_CACHE.with(|active| *active.borrow_mut() = Some(entry));
}
pub(crate) fn deactivate_brush_cache() {
ACTIVE_BRUSH_CACHE.with(|active| *active.borrow_mut() = None);
}
fn get_or_create_brush(
target: &ID2D1RenderTarget,
color: Color,
) -> WinResult<ID2D1SolidColorBrush> {
let raw = target.as_raw() as usize;
let cache = ACTIVE_BRUSH_CACHE.with(|active| match &*active.borrow() {
Some((owner, weak)) if *owner == raw => weak.upgrade(),
_ => None,
});
if let Some(brush) = cache
.as_ref()
.and_then(|cache| cache.borrow().get(&color).cloned())
{
return Ok(brush);
}
let brush = unsafe { target.CreateSolidColorBrush(&color_to_d2d(color), None)? };
BRUSH_CREATIONS.with(|count| count.set(count.get() + 1));
if let Some(cache) = cache {
cache.borrow_mut().insert(color, brush.clone());
}
Ok(brush)
}
#[cfg(test)]
pub(crate) fn brush_creation_count() -> u32 {
BRUSH_CREATIONS.with(|count| count.get())
}
#[cfg(test)]
pub(crate) fn reset_brush_creation_count() {
BRUSH_CREATIONS.with(|count| count.set(0));
}
pub(crate) fn fill_rect(target: &ID2D1RenderTarget, rect: Rect, color: Color) -> WinResult<()> {
let brush = get_or_create_brush(target, color)?;
let rect_f = D2D_RECT_F {
left: rect.x,
top: rect.y,
right: rect.x + rect.width,
bottom: rect.y + rect.height,
};
unsafe { target.FillRectangle(&rect_f, &brush) };
Ok(())
}
pub(crate) fn fill_rounded_rect(
target: &ID2D1RenderTarget,
rect: Rect,
radius: f32,
color: Color,
) -> WinResult<()> {
let brush = get_or_create_brush(target, color)?;
let r = radius.min(rect.width / 2.0).min(rect.height / 2.0).max(0.0);
let rounded = D2D1_ROUNDED_RECT {
rect: D2D_RECT_F {
left: rect.x,
top: rect.y,
right: rect.x + rect.width,
bottom: rect.y + rect.height,
},
radiusX: r,
radiusY: r,
};
unsafe { target.FillRoundedRectangle(&rounded, &brush) };
Ok(())
}
pub(crate) fn stroke_rect(
target: &ID2D1RenderTarget,
rect: Rect,
color: Color,
stroke_width: f32,
) -> WinResult<()> {
let brush = get_or_create_brush(target, color)?;
let inset = (stroke_width / 2.0)
.min(rect.width / 2.0)
.min(rect.height / 2.0)
.max(0.0);
let rect_f = D2D_RECT_F {
left: rect.x + inset,
top: rect.y + inset,
right: rect.x + rect.width - inset,
bottom: rect.y + rect.height - inset,
};
unsafe { target.DrawRectangle(&rect_f, &brush, stroke_width, None) };
Ok(())
}
pub(crate) fn stroke_rounded_rect(
target: &ID2D1RenderTarget,
rect: Rect,
radius: f32,
color: Color,
stroke_width: f32,
) -> WinResult<()> {
let brush = get_or_create_brush(target, color)?;
let inset = (stroke_width / 2.0)
.min(rect.width / 2.0)
.min(rect.height / 2.0)
.max(0.0);
let r = (radius - inset)
.min(rect.width / 2.0 - inset)
.min(rect.height / 2.0 - inset)
.max(0.0);
let rounded = D2D1_ROUNDED_RECT {
rect: D2D_RECT_F {
left: rect.x + inset,
top: rect.y + inset,
right: rect.x + rect.width - inset,
bottom: rect.y + rect.height - inset,
},
radiusX: r,
radiusY: r,
};
unsafe { target.DrawRoundedRectangle(&rounded, &brush, stroke_width, None) };
Ok(())
}
pub(crate) fn push_clip(target: &ID2D1RenderTarget, rect: Rect) {
let rect_f = D2D_RECT_F {
left: rect.x,
top: rect.y,
right: rect.x + rect.width,
bottom: rect.y + rect.height,
};
unsafe { target.PushAxisAlignedClip(&rect_f, D2D1_ANTIALIAS_MODE_PER_PRIMITIVE) };
}
pub(crate) fn pop_clip(target: &ID2D1RenderTarget) {
unsafe { target.PopAxisAlignedClip() };
}
pub(crate) fn draw_line(
target: &ID2D1RenderTarget,
x0: f32,
y0: f32,
x1: f32,
y1: f32,
color: Color,
stroke_width: f32,
) -> WinResult<()> {
let brush = get_or_create_brush(target, color)?;
unsafe {
target.DrawLine(
Vector2 { X: x0, Y: y0 },
Vector2 { X: x1, Y: y1 },
&brush,
stroke_width,
None,
);
}
Ok(())
}
thread_local! {
static CURRENT_TRANSLATION: Cell<(f32, f32)> = const { Cell::new((0.0, 0.0)) };
}
pub(crate) fn with_translation<F: FnOnce() -> R, R>(
target: &ID2D1RenderTarget,
dx: f32,
dy: f32,
f: F,
) -> R {
let translated = windows_numerics::Matrix3x2 {
M11: 1.0,
M12: 0.0,
M21: 0.0,
M22: 1.0,
M31: dx,
M32: dy,
};
unsafe { target.SetTransform(&translated) };
let prev = CURRENT_TRANSLATION.get();
CURRENT_TRANSLATION.set((dx, dy));
let result = f();
CURRENT_TRANSLATION.set(prev);
let identity = windows_numerics::Matrix3x2 {
M11: 1.0,
M12: 0.0,
M21: 0.0,
M22: 1.0,
M31: 0.0,
M32: 0.0,
};
unsafe { target.SetTransform(&identity) };
result
}
pub(crate) fn with_horizontal_scale<F: FnOnce()>(
target: &ID2D1RenderTarget,
scale_x: f32,
anchor_x: f32,
f: F,
) {
let scaled = windows_numerics::Matrix3x2 {
M11: scale_x,
M12: 0.0,
M21: 0.0,
M22: 1.0,
M31: anchor_x * (1.0 - scale_x),
M32: 0.0,
};
unsafe { target.SetTransform(&scaled) };
f();
let identity = windows_numerics::Matrix3x2 {
M11: 1.0,
M12: 0.0,
M21: 0.0,
M22: 1.0,
M31: 0.0,
M32: 0.0,
};
unsafe { target.SetTransform(&identity) };
}
#[cfg(test)]
mod tests {
use super::*;
use crate::win::testing::HeadlessSurface;
#[test]
fn stroke_rect_paints_a_crisp_border_inside_the_bounds() {
const BG: Color = Color::rgb(10, 20, 30);
const BORDER: Color = Color::rgb(200, 100, 50);
let surface = HeadlessSurface::new(32, 32).expect("create surface");
surface
.paint(|target| {
let _ = fill_rect(target, Rect::new(0.0, 0.0, 32.0, 32.0), BG);
let _ = stroke_rect(target, Rect::new(8.0, 8.0, 16.0, 16.0), BORDER, 1.0);
})
.expect("paint");
let rgb = |x: u32, y: u32| {
let c = surface.pixel_at(x, y);
(c.r, c.g, c.b)
};
let border = (BORDER.r, BORDER.g, BORDER.b);
let bg = (BG.r, BG.g, BG.b);
assert_eq!(rgb(16, 8), border, "top edge");
assert_eq!(rgb(16, 23), border, "bottom edge");
assert_eq!(rgb(8, 16), border, "left edge");
assert_eq!(rgb(23, 16), border, "right edge");
assert_eq!(rgb(16, 7), bg, "one row above the top edge");
assert_eq!(rgb(7, 16), bg, "one column left of the left edge");
assert_eq!(rgb(24, 16), bg, "one column right of the right edge");
assert_eq!(rgb(16, 24), bg, "one row below the bottom edge");
assert_eq!(rgb(16, 9), bg, "one row inside the top edge");
assert_eq!(rgb(9, 16), bg, "one column inside the left edge");
}
#[test]
fn with_horizontal_scale_restores_identity_transform_after() {
let surface = HeadlessSurface::new(50, 50).expect("create surface");
surface
.paint(|target| {
with_horizontal_scale(target, 1.5, 10.0, || {
});
let mut m = windows_numerics::Matrix3x2 {
M11: 0.0,
M12: 0.0,
M21: 0.0,
M22: 0.0,
M31: 0.0,
M32: 0.0,
};
unsafe { target.GetTransform(&mut m) };
assert_eq!(
(m.M11, m.M12, m.M21, m.M22, m.M31, m.M32),
(1.0, 0.0, 0.0, 1.0, 0.0, 0.0),
"transform must be identity after with_horizontal_scale returns"
);
})
.expect("paint");
}
#[test]
fn with_horizontal_scale_anchors_at_the_given_x() {
let surface = HeadlessSurface::new(50, 50).expect("create surface");
surface
.paint(|target| {
with_horizontal_scale(target, 2.0, 10.0, || {
let mut m = windows_numerics::Matrix3x2 {
M11: 0.0,
M12: 0.0,
M21: 0.0,
M22: 0.0,
M31: 0.0,
M32: 0.0,
};
unsafe { target.GetTransform(&mut m) };
let map_x = |x: f32| x * m.M11 + m.M31;
assert!(
(map_x(10.0) - 10.0).abs() < 1e-6,
"anchor point must map to itself"
);
assert!(
(map_x(11.0) - 12.0).abs() < 1e-6,
"one DIP right of the anchor must move by scale_x DIPs"
);
});
})
.expect("paint");
}
#[test]
fn draw_text_paints_a_label_sized_to_its_own_measured_width() {
const BG: Color = Color::rgb(10, 20, 30);
const FG: Color = Color::rgb(220, 40, 40);
let (dwrite, _, _) = DWrite::new("Segoe UI", 10.0, None).expect("create DWrite");
let text = " OK ";
let (width, height) = dwrite.measure_text(text).expect("measure_text");
let x = 129.454_42_f32;
let rect = Rect::new(x, 4.0, width, height.max(1.0));
let surface = HeadlessSurface::new(220, 40).expect("create surface");
surface
.paint(|target| {
let _ = fill_rect(target, Rect::new(0.0, 0.0, 220.0, 40.0), BG);
dwrite.draw_text(target, text, rect, FG).expect("draw_text");
})
.expect("paint");
let bg = (BG.r, BG.g, BG.b);
let found = (rect.y as u32..(rect.y + rect.height) as u32)
.flat_map(|y| (rect.x as u32..(rect.x + rect.width) as u32).map(move |x| (x, y)))
.map(|(x, y)| surface.pixel_at(x, y))
.any(|px| (px.r, px.g, px.b) != bg);
assert!(
found,
"a label box sized to exactly its own measured width must still paint at \
least one non-background pixel"
);
}
#[test]
fn codicon_close_glyph_resolves_through_the_registered_fallback_and_paints_ink() {
const BG: Color = Color::rgb(10, 20, 30);
const FG: Color = Color::rgb(220, 40, 40);
let (collection, names) = register_font_from_memory(crate::codicon::FONT_BYTES)
.expect("the bundled codicon.ttf must register with DirectWrite");
assert!(
names.iter().any(|n| n == crate::codicon::FONT_FAMILY),
"registered family names {names:?} should include {:?}",
crate::codicon::FONT_FAMILY
);
let fallback =
build_nerd_font_fallback_multi(&[(crate::codicon::FONT_FAMILY, Some(&collection))])
.expect("build a fallback mapping just the registered codicon family");
let (dwrite, _, _) = DWrite::new("Segoe UI", 16.0, Some(&fallback)).expect("create DWrite");
let glyph = crate::codicon::CLOSE.to_string();
let (width, height) = dwrite.measure_text(&glyph).expect("measure_text");
let rect = Rect::new(4.0, 4.0, width.max(1.0), height.max(1.0));
let surface = HeadlessSurface::new(40, 40).expect("create surface");
surface
.paint(|target| {
let _ = fill_rect(target, Rect::new(0.0, 0.0, 40.0, 40.0), BG);
dwrite
.draw_text(target, &glyph, rect, FG)
.expect("draw_text");
})
.expect("paint");
let bg = (BG.r, BG.g, BG.b);
let found = (rect.y as u32..(rect.y + rect.height) as u32)
.flat_map(|y| (rect.x as u32..(rect.x + rect.width) as u32).map(move |x| (x, y)))
.map(|(x, y)| surface.pixel_at(x, y))
.any(|px| (px.r, px.g, px.b) != bg);
assert!(
found,
"the codicon close glyph, painted through the registered-font fallback, must \
paint at least one non-background pixel"
);
}
#[test]
fn draw_text_clips_an_overlong_run_instead_of_wrapping_it_away() {
const BG: Color = Color::rgb(10, 20, 30);
const FG: Color = Color::rgb(220, 40, 40);
let (dwrite, _, _) = DWrite::new("Segoe UI", 10.0, None).expect("create DWrite");
let text = "This label is far too long to fit in the narrow box below it";
let (_, natural_height) = dwrite.measure_text(text).expect("measure_text");
let rect = Rect::new(4.0, 4.0, 12.0, natural_height.max(1.0));
let surface = HeadlessSurface::new(220, 40).expect("create surface");
surface
.paint(|target| {
let _ = fill_rect(target, Rect::new(0.0, 0.0, 220.0, 40.0), BG);
dwrite.draw_text(target, text, rect, FG).expect("draw_text");
})
.expect("paint");
let bg = (BG.r, BG.g, BG.b);
let ink_in_box = (rect.y as u32..(rect.y + rect.height) as u32)
.flat_map(|y| (rect.x as u32..(rect.x + rect.width) as u32).map(move |x| (x, y)))
.map(|(x, y)| surface.pixel_at(x, y))
.any(|px| (px.r, px.g, px.b) != bg);
assert!(
ink_in_box,
"a box far narrower than the text's first word must still paint the visible \
prefix that fits (clipped at the right edge), not vanish entirely because \
the first word reflowed onto a line this single-line-tall box can't show"
);
}
#[test]
fn minimap_text_format_caches_by_rounded_px() {
let (dwrite, _, _) = DWrite::new("Segoe UI", 10.0, None).expect("create DWrite");
let a = dwrite.minimap_text_format(6.0).expect("format at 6px");
let b = dwrite
.minimap_text_format(6.0)
.expect("format at 6px again");
assert_eq!(
a.as_raw(),
b.as_raw(),
"the same rounded px must reuse the cached format"
);
assert_eq!(dwrite.minimap_format_cache_len(), 1);
let c = dwrite.minimap_text_format(12.0).expect("format at 12px");
assert_ne!(
a.as_raw(),
c.as_raw(),
"a different rounded px must create a new format"
);
assert_eq!(dwrite.minimap_format_cache_len(), 2);
}
}