piet_direct2d/
dwrite.rs

1// Copyright 2020 the Piet Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Convenience wrappers for DirectWrite objects.
5
6// TODO: get rid of this when we actually do use everything
7#![allow(unused)]
8
9use std::convert::TryInto;
10use std::ffi::OsString;
11use std::fmt::{Debug, Display, Formatter};
12use std::mem::MaybeUninit;
13use std::ptr::null_mut;
14use std::sync::Arc;
15
16use dwrote::FontCollection as DWFontCollection;
17use winapi::Interface;
18use winapi::shared::minwindef::{FALSE, TRUE};
19use winapi::shared::ntdef::LOCALE_NAME_MAX_LENGTH;
20use winapi::shared::winerror::{HRESULT, S_OK, SUCCEEDED};
21use winapi::um::dwrite::{
22    DWRITE_FACTORY_TYPE_SHARED, DWRITE_FONT_STRETCH_NORMAL, DWRITE_FONT_STYLE,
23    DWRITE_FONT_STYLE_ITALIC, DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_WEIGHT,
24    DWRITE_FONT_WEIGHT_NORMAL, DWRITE_HIT_TEST_METRICS, DWRITE_LINE_METRICS,
25    DWRITE_OVERHANG_METRICS, DWRITE_READING_DIRECTION_RIGHT_TO_LEFT, DWRITE_TEXT_ALIGNMENT_CENTER,
26    DWRITE_TEXT_ALIGNMENT_JUSTIFIED, DWRITE_TEXT_ALIGNMENT_LEADING, DWRITE_TEXT_ALIGNMENT_TRAILING,
27    DWRITE_TEXT_METRICS, DWRITE_TEXT_RANGE, DWriteCreateFactory, IDWriteFactory,
28    IDWriteFontCollection, IDWriteFontFamily, IDWriteLocalizedStrings, IDWriteTextFormat,
29    IDWriteTextLayout,
30};
31use winapi::um::unknwnbase::IUnknown;
32use winapi::um::winnls::GetUserDefaultLocaleName;
33
34use wio::com::ComPtr;
35use wio::wide::{FromWide, ToWide};
36
37use piet::kurbo::Insets;
38use piet::{FontFamily as PietFontFamily, FontStyle, FontWeight, TextAlignment};
39
40use crate::Brush;
41
42/// "en-US" as null-terminated utf16.
43const DEFAULT_LOCALE: &[u16] = &utf16_lit::utf16_null!("en-US");
44
45/// The max layout constraint we use with dwrite.
46///
47/// On other platforms we use infinity, but on dwrite that contaminates
48/// the values that we get back when calculating the image bounds.
49// approximately the largest f32 without integer error
50const MAX_LAYOUT_CONSTRAINT: f32 = 1.6e7;
51
52// TODO: minimize cut'n'paste; probably the best way to do this is
53// unify with the crate error type
54pub enum Error {
55    WinapiError(HRESULT),
56}
57
58/// This struct is public only to use for system integration in piet_common and druid-shell. It is not intended
59/// that end-users directly use this struct.
60#[derive(Clone)]
61pub struct DwriteFactory(ComPtr<IDWriteFactory>);
62
63// I couldn't find any documentation about using IDWriteFactory in a multi-threaded context.
64// Hopefully, `Send` is a conservative enough assumption.
65unsafe impl Send for DwriteFactory {}
66
67#[derive(Clone)]
68pub struct TextFormat(pub(crate) ComPtr<IDWriteTextFormat>);
69
70#[derive(Clone)]
71struct FontFamily(ComPtr<IDWriteFontFamily>);
72
73pub struct FontCollection(ComPtr<IDWriteFontCollection>);
74
75#[derive(Clone)]
76pub struct TextLayout(ComPtr<IDWriteTextLayout>);
77
78/// A range in a windows string, represented as a start position and a length.
79#[derive(Debug, Clone, Copy)]
80pub struct Utf16Range {
81    pub start: usize,
82    pub len: usize,
83}
84
85impl From<HRESULT> for Error {
86    fn from(hr: HRESULT) -> Error {
87        Error::WinapiError(hr)
88    }
89}
90
91impl Debug for Error {
92    fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
93        match self {
94            Error::WinapiError(hr) => write!(f, "hresult {hr:x}"),
95        }
96    }
97}
98
99impl Display for Error {
100    fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
101        match self {
102            Error::WinapiError(hr) => write!(f, "hresult {hr:x}"),
103        }
104    }
105}
106
107impl std::error::Error for Error {
108    fn description(&self) -> &str {
109        "winapi error"
110    }
111}
112
113impl From<Error> for piet::Error {
114    fn from(e: Error) -> piet::Error {
115        piet::Error::BackendError(Box::new(e))
116    }
117}
118
119unsafe fn wrap<T, U, F>(hr: HRESULT, ptr: *mut T, f: F) -> Result<U, Error>
120where
121    F: Fn(ComPtr<T>) -> U,
122    T: Interface,
123{
124    if SUCCEEDED(hr) {
125        Ok(f(unsafe { ComPtr::from_raw(ptr) }))
126    } else {
127        Err(hr.into())
128    }
129}
130
131impl DwriteFactory {
132    pub fn new() -> Result<DwriteFactory, Error> {
133        unsafe {
134            let mut ptr: *mut IDWriteFactory = null_mut();
135            let hr = DWriteCreateFactory(
136                DWRITE_FACTORY_TYPE_SHARED,
137                &IDWriteFactory::uuidof(),
138                &mut ptr as *mut _ as *mut _,
139            );
140            wrap(hr, ptr, DwriteFactory)
141        }
142    }
143
144    pub fn get_raw(&self) -> *mut IDWriteFactory {
145        self.0.as_raw()
146    }
147
148    pub(crate) fn system_font_collection(&self) -> Result<FontCollection, Error> {
149        unsafe {
150            let mut ptr = null_mut();
151            let hr = self.0.GetSystemFontCollection(&mut ptr, 0);
152            wrap(hr, ptr, FontCollection)
153        }
154    }
155
156    /// Create from raw pointer
157    ///
158    /// # Safety
159    /// TODO
160    pub unsafe fn from_raw(raw: *mut IDWriteFactory) -> Self {
161        Self(unsafe { ComPtr::from_raw(raw) })
162    }
163}
164
165impl FontCollection {
166    pub(crate) fn font_family(&self, name: &str) -> Option<PietFontFamily> {
167        let wname = name.to_wide_null();
168        let mut idx = u32::MAX;
169        let mut exists = 0_i32;
170
171        let family = unsafe {
172            let hr = self.0.FindFamilyName(wname.as_ptr(), &mut idx, &mut exists);
173            if SUCCEEDED(hr) && exists != 0 {
174                let mut family = null_mut();
175                let hr = self.0.GetFontFamily(idx, &mut family);
176                wrap(hr, family, FontFamily).ok()
177            } else {
178                eprintln!(
179                    "failed to find family name {}: err {} not_found: {}",
180                    name, hr, !exists
181                );
182                None
183            }
184        }?;
185
186        family.family_name().ok()
187    }
188}
189
190impl FontFamily {
191    // this monster is taken right out of the docs :/
192    /// Returns the localized name of this family.
193    fn family_name(&self) -> Result<PietFontFamily, Error> {
194        unsafe {
195            let mut names = null_mut();
196            let hr = self.0.GetFamilyNames(&mut names);
197            if !SUCCEEDED(hr) {
198                return Err(hr.into());
199            }
200
201            let names: ComPtr<IDWriteLocalizedStrings> = ComPtr::from_raw(names);
202
203            let mut index = 0_u32;
204            let mut exists = 0_i32;
205            let mut locale_name = [0_u16; LOCALE_NAME_MAX_LENGTH];
206
207            let success =
208                GetUserDefaultLocaleName(locale_name.as_mut_ptr(), LOCALE_NAME_MAX_LENGTH as i32);
209            let mut hr = if SUCCEEDED(success) {
210                names.FindLocaleName(locale_name.as_ptr(), &mut index, &mut exists)
211            } else {
212                // we reuse the previous success; we want  to run the next block
213                // both if the previous failed, or if it just doesn't find anything
214                hr
215            };
216            if !SUCCEEDED(hr) || exists == 0 {
217                hr = names.FindLocaleName(DEFAULT_LOCALE.as_ptr(), &mut index, &mut exists);
218            }
219
220            if !SUCCEEDED(hr) {
221                return Err(hr.into());
222            }
223
224            // if locale doesn't exist, just choose the first
225            if exists == 0 {
226                index = 0;
227            }
228
229            let mut length = 0_u32;
230            let hr = names.GetStringLength(index, &mut length);
231
232            if !SUCCEEDED(hr) {
233                return Err(hr.into());
234            }
235
236            let mut wide_name: Vec<u16> = Vec::with_capacity(length as usize + 1);
237            let hr = names.GetString(index, wide_name.as_mut_ptr(), length + 1);
238            if SUCCEEDED(hr) {
239                wide_name.set_len(length as usize + 1);
240                let name = OsString::from_wide(&wide_name)
241                    .into_string()
242                    .unwrap_or_else(|err| err.to_string_lossy().into_owned());
243
244                Ok(PietFontFamily::new_unchecked(name))
245            } else {
246                Err(hr.into())
247            }
248        }
249    }
250}
251
252impl TextFormat {
253    pub(crate) fn new(
254        factory: &DwriteFactory,
255        family: impl AsRef<[u16]>,
256        size: f32,
257        rtl: bool,
258    ) -> Result<TextFormat, Error> {
259        let family = family.as_ref();
260
261        unsafe {
262            let mut ptr = null_mut();
263            let hr = factory.0.CreateTextFormat(
264                family.as_ptr(),
265                null_mut(), // collection
266                DWRITE_FONT_WEIGHT_NORMAL,
267                DWRITE_FONT_STYLE_NORMAL,
268                DWRITE_FONT_STRETCH_NORMAL,
269                size,
270                //TODO: this should be the user's locale? It will influence font fallback behaviour?
271                DEFAULT_LOCALE.as_ptr(),
272                &mut ptr,
273            );
274
275            let r = wrap(hr, ptr, TextFormat)?;
276            if rtl {
277                r.0.SetReadingDirection(DWRITE_READING_DIRECTION_RIGHT_TO_LEFT);
278            }
279            Ok(r)
280        }
281    }
282}
283
284#[allow(overflowing_literals)]
285#[allow(clippy::unreadable_literal)]
286const E_NOT_SUFFICIENT_BUFFER: HRESULT = 0x8007007A;
287
288impl Utf16Range {
289    pub fn new(start: usize, len: usize) -> Self {
290        Utf16Range { start, len }
291    }
292}
293
294impl From<Utf16Range> for DWRITE_TEXT_RANGE {
295    fn from(src: Utf16Range) -> DWRITE_TEXT_RANGE {
296        let Utf16Range { start, len } = src;
297        DWRITE_TEXT_RANGE {
298            startPosition: start.try_into().unwrap(),
299            length: len.try_into().unwrap(),
300        }
301    }
302}
303
304impl TextLayout {
305    pub(crate) fn new(
306        dwrite: &DwriteFactory,
307        format: TextFormat,
308        width: f32,
309        text: &[u16],
310    ) -> Result<Self, Error> {
311        let len: u32 = text.len().try_into().unwrap();
312        // d2d doesn't handle infinity very well
313        let width = if !width.is_finite() {
314            MAX_LAYOUT_CONSTRAINT
315        } else {
316            width
317        };
318
319        unsafe {
320            let mut ptr = null_mut();
321            let hr = dwrite.0.CreateTextLayout(
322                text.as_ptr(),
323                len,
324                format.0.as_raw(),
325                width,
326                MAX_LAYOUT_CONSTRAINT,
327                &mut ptr,
328            );
329            wrap(hr, ptr, TextLayout)
330        }
331    }
332
333    /// Set the alignment for this entire layout.
334    pub(crate) fn set_alignment(&mut self, alignment: TextAlignment) {
335        let alignment = match alignment {
336            TextAlignment::Start => DWRITE_TEXT_ALIGNMENT_LEADING,
337            TextAlignment::End => DWRITE_TEXT_ALIGNMENT_TRAILING,
338            TextAlignment::Center => DWRITE_TEXT_ALIGNMENT_CENTER,
339            TextAlignment::Justified => DWRITE_TEXT_ALIGNMENT_JUSTIFIED,
340        };
341
342        unsafe {
343            self.0.SetTextAlignment(alignment);
344        }
345    }
346
347    /// Set the weight for a range of this layout. `start` and `len` are in utf16.
348    pub(crate) fn set_weight(&mut self, range: Utf16Range, weight: FontWeight) {
349        let weight = weight.to_raw() as DWRITE_FONT_WEIGHT;
350        unsafe {
351            self.0.SetFontWeight(weight, range.into());
352        }
353    }
354
355    pub(crate) fn set_font_family(&mut self, range: Utf16Range, family: &str) {
356        let wide_name = family.to_wide_null();
357        unsafe {
358            self.0.SetFontFamilyName(wide_name.as_ptr(), range.into());
359        }
360    }
361
362    pub(crate) fn set_font_collection(&mut self, range: Utf16Range, collection: &DWFontCollection) {
363        unsafe {
364            self.0.SetFontCollection(collection.as_ptr(), range.into());
365        }
366    }
367
368    pub(crate) fn set_style(&mut self, range: Utf16Range, style: FontStyle) {
369        let val = match style {
370            FontStyle::Italic => DWRITE_FONT_STYLE_ITALIC,
371            FontStyle::Regular => DWRITE_FONT_STYLE_NORMAL,
372        };
373        unsafe {
374            self.0.SetFontStyle(val, range.into());
375        }
376    }
377
378    pub(crate) fn set_underline(&mut self, range: Utf16Range, flag: bool) {
379        let flag = if flag { TRUE } else { FALSE };
380        unsafe {
381            self.0.SetUnderline(flag, range.into());
382        }
383    }
384
385    pub(crate) fn set_strikethrough(&mut self, range: Utf16Range, flag: bool) {
386        let flag = if flag { TRUE } else { FALSE };
387        unsafe {
388            self.0.SetStrikethrough(flag, range.into());
389        }
390    }
391
392    pub(crate) fn set_size(&mut self, range: Utf16Range, size: f32) {
393        unsafe {
394            self.0.SetFontSize(size, range.into());
395        }
396    }
397
398    pub(crate) fn set_foreground_brush(&mut self, range: Utf16Range, brush: Brush) {
399        unsafe {
400            self.0
401                .SetDrawingEffect(brush.as_raw() as *mut IUnknown, range.into());
402        }
403    }
404
405    /// Get line metrics, storing them in the provided buffer.
406    ///
407    /// Note: this isn't necessarily the lowest level wrapping, as it requires
408    /// an allocation for the buffer. But it's pretty ergonomic.
409    pub fn get_line_metrics(&self, buf: &mut Vec<DWRITE_LINE_METRICS>) {
410        let cap = buf.capacity().min(0xffff_ffff) as u32;
411        unsafe {
412            let mut actual_count = 0;
413            let mut hr = self
414                .0
415                .GetLineMetrics(buf.as_mut_ptr(), cap, &mut actual_count);
416            if hr == E_NOT_SUFFICIENT_BUFFER {
417                buf.reserve(actual_count as usize - buf.len());
418                hr = self
419                    .0
420                    .GetLineMetrics(buf.as_mut_ptr(), actual_count, &mut actual_count);
421            }
422            if SUCCEEDED(hr) {
423                buf.set_len(actual_count as usize);
424            } else {
425                buf.set_len(0);
426            }
427        }
428    }
429
430    pub fn get_raw(&self) -> *mut IDWriteTextLayout {
431        self.0.as_raw()
432    }
433
434    pub fn get_metrics(&self) -> DWRITE_TEXT_METRICS {
435        unsafe {
436            let mut result = std::mem::zeroed();
437            self.0.GetMetrics(&mut result);
438            result
439        }
440    }
441
442    /// Return the DWRITE_OVERHANG_METRICS, converted to an `Insets` struct.
443    ///
444    /// The 'right' and 'bottom' values of this struct are relative to the *layout*
445    /// width and height; that is, the width and height constraints used to create
446    /// the layout, not the actual size of the generated layout.
447    pub fn get_overhang_metrics(&self) -> Insets {
448        unsafe {
449            let mut result = std::mem::zeroed();
450            // returning all 0s on failure feels okay?
451            let _ = self.0.GetOverhangMetrics(&mut result);
452            let DWRITE_OVERHANG_METRICS {
453                left,
454                top,
455                right,
456                bottom,
457            } = result;
458            Insets::new(left as f64, top as f64, right as f64, bottom as f64)
459        }
460    }
461
462    pub fn set_max_width(&mut self, max_width: f64) -> Result<(), Error> {
463        // infinity produces nonsense values for the inking rect on d2d
464        let max_width = if !max_width.is_finite() {
465            MAX_LAYOUT_CONSTRAINT
466        } else {
467            max_width as f32
468        };
469
470        unsafe {
471            let hr = self.0.SetMaxWidth(max_width);
472
473            if SUCCEEDED(hr) {
474                Ok(())
475            } else {
476                Err(hr.into())
477            }
478        }
479    }
480
481    pub fn hit_test_point(&self, point_x: f32, point_y: f32) -> HitTestPoint {
482        unsafe {
483            let mut trail = 0;
484            let mut inside = 0;
485            let mut metrics = MaybeUninit::uninit();
486            self.0.HitTestPoint(
487                point_x,
488                point_y,
489                &mut trail,
490                &mut inside,
491                metrics.as_mut_ptr(),
492            );
493
494            HitTestPoint {
495                metrics: metrics.assume_init().into(),
496                is_inside: inside != 0,
497                is_trailing_hit: trail != 0,
498            }
499        }
500    }
501
502    pub fn hit_test_text_position(
503        &self,
504        position: u32,
505        trailing: bool,
506    ) -> Option<HitTestTextPosition> {
507        let trailing = trailing as i32;
508        unsafe {
509            let (mut x, mut y) = (0.0, 0.0);
510            let mut metrics = std::mem::zeroed();
511            let res = self
512                .0
513                .HitTestTextPosition(position, trailing, &mut x, &mut y, &mut metrics);
514            if res != S_OK {
515                return None;
516            }
517
518            Some(HitTestTextPosition {
519                metrics: metrics.into(),
520                point_x: x,
521                point_y: y,
522            })
523        }
524    }
525}
526
527#[derive(Copy, Clone)]
528/// Results from calling `hit_test_point` on a TextLayout.
529pub struct HitTestPoint {
530    /// The output geometry fully enclosing the hit-test location. When is_inside is set to false,
531    /// this structure represents the geometry enclosing the edge closest to the hit-test location.
532    pub metrics: HitTestMetrics,
533    /// An output flag that indicates whether the hit-test location is inside the text string. When
534    /// false, the position nearest the text's edge is returned.
535    pub is_inside: bool,
536    /// An output flag that indicates whether the hit-test location is at the leading or the
537    /// trailing side of the character. When is_inside is set to false, this value is set according
538    /// to the output hitTestMetrics->textPosition value to represent the edge closest to the
539    /// hit-test location.
540    pub is_trailing_hit: bool,
541}
542
543#[derive(Copy, Clone)]
544/// Results from calling `hit_test_text_position` on a TextLayout.
545pub struct HitTestTextPosition {
546    /// The output pixel location X, relative to the top-left location of the layout box.
547    pub point_x: f32,
548    /// The output pixel location Y, relative to the top-left location of the layout box.
549    pub point_y: f32,
550
551    /// The output geometry fully enclosing the specified text position.
552    pub metrics: HitTestMetrics,
553}
554
555#[repr(C)]
556#[derive(Copy, Clone, Debug)]
557/// Describes the region obtained by a hit test.
558pub struct HitTestMetrics {
559    /// The first text position within the hit region.
560    pub text_position: u32,
561    /// The number of text positions within the hit region.
562    pub length: u32,
563    /// The x-coordinate of the upper-left corner of the hit region.
564    pub left: f32,
565    /// The y-coordinate of the upper-left corner of the hit region.
566    pub top: f32,
567    /// The width of the hit region.
568    pub width: f32,
569    /// The height of the hit region.
570    pub height: f32,
571    /// The BIDI level of the text positions within the hit region.
572    pub bidi_level: u32,
573    /// Non-zero if the hit region contains text; otherwise, `0`.
574    pub is_text: bool,
575    /// Non-zero if the text range is trimmed; otherwise, `0`.
576    pub is_trimmed: bool,
577}
578
579impl From<DWRITE_HIT_TEST_METRICS> for HitTestMetrics {
580    fn from(metrics: DWRITE_HIT_TEST_METRICS) -> Self {
581        HitTestMetrics {
582            text_position: metrics.textPosition,
583            length: metrics.length,
584            left: metrics.left,
585            top: metrics.top,
586            width: metrics.width,
587            height: metrics.height,
588            bidi_level: metrics.bidiLevel,
589            is_text: metrics.isText != 0,
590            is_trimmed: metrics.isTrimmed != 0,
591        }
592    }
593}
594
595#[cfg(test)]
596mod tests {
597    use super::*;
598
599    #[test]
600    fn family_names() {
601        let factory = DwriteFactory::new().unwrap();
602        let fonts = factory.system_font_collection().unwrap();
603        assert!(fonts.font_family("serif").is_none());
604        assert!(fonts.font_family("arial").is_some());
605        assert!(fonts.font_family("Arial").is_some());
606        assert!(fonts.font_family("Times New Roman").is_some());
607    }
608
609    #[test]
610    fn default_locale() {
611        assert_eq!("en-US".to_wide_null().as_slice(), DEFAULT_LOCALE);
612    }
613}