textwidth/
lib.rs

1use std::ffi::CString;
2use std::mem::MaybeUninit;
3use std::ptr;
4use thiserror::Error;
5use x11::xlib;
6
7/// XError holds the X11 error message
8#[derive(Debug, Error)]
9pub enum XError {
10    /// No X11 display found
11    #[error("X Error: Could not open Display")]
12    DisplayOpen,
13
14    /// The font could not be found
15    #[error("X Error: Could not load font with name {0:?}")]
16    CouldNotLoadFont(CString),
17
18    /// This error is returned when the string you pass cannot be converted to a CString
19    #[error("CStrings cannot hold NUL values")]
20    NulError(#[from] std::ffi::NulError),
21}
22
23static_assertions::assert_impl_all!(XError: Sync, Send);
24
25enum Data {
26    FontSet {
27        display: *mut xlib::Display,
28        fontset: xlib::XFontSet,
29    },
30    XFont {
31        display: *mut xlib::Display,
32        xfont: *mut xlib::XFontStruct,
33    },
34}
35
36/// A context, holding the internal data required to query a string
37pub struct Context {
38    data: Data,
39}
40
41impl Context {
42    /// Creates a new context given by the font string given here.
43    ///
44    /// The font string should be of the X11 form, as selected by `fontsel`.
45    /// XFT is not supported!
46    pub fn new(name: &str) -> Result<Self, XError> {
47        let name: CString = CString::new(name)?;
48        // SAFE because we simply call the
49        let dpy = unsafe { xlib::XOpenDisplay(ptr::null()) };
50        if dpy.is_null() {
51            return Err(XError::DisplayOpen);
52        }
53        let mut missing_ptr = MaybeUninit::uninit();
54        let mut missing_len = MaybeUninit::uninit();
55        // SAFE because values are correct
56        let fontset = unsafe {
57            xlib::XCreateFontSet(
58                dpy,
59                name.as_ptr(),
60                missing_ptr.as_mut_ptr(),
61                missing_len.as_mut_ptr(),
62                ptr::null_mut(),
63            )
64        };
65
66        // SAFE because XCreateFontSet always sets both ptrs to NULL or a valid value
67        unsafe {
68            if !missing_ptr.assume_init().is_null() {
69                xlib::XFreeStringList(missing_ptr.assume_init());
70            }
71        }
72        if !fontset.is_null() {
73            Ok(Context {
74                data: Data::FontSet {
75                    display: dpy,
76                    fontset,
77                },
78            })
79        } else {
80            // SAFE as both dpy and name are valid
81            let xfont = unsafe { xlib::XLoadQueryFont(dpy, name.as_ptr()) };
82            if xfont.is_null() {
83                // SAFE as dpy is a valid display
84                unsafe { xlib::XCloseDisplay(dpy) };
85                Err(XError::CouldNotLoadFont(name))
86            } else {
87                Ok(Context {
88                    data: Data::XFont {
89                        display: dpy,
90                        xfont,
91                    },
92                })
93            }
94        }
95    }
96
97    /// Creates a new context with the misc-fixed font.
98    pub fn with_misc() -> Result<Self, XError> {
99        Self::new("-misc-fixed-*-*-*-*-*-*-*-*-*-*-*-*")
100    }
101
102    /// Get text width for the given string
103    pub fn text_width<S: AsRef<str>>(&self, text: S) -> Result<u64, XError> {
104        get_text_width(&self, text)
105    }
106}
107
108impl Drop for Context {
109    fn drop(&mut self) {
110        unsafe {
111            match self.data {
112                Data::FontSet { display, fontset } => {
113                    xlib::XFreeFontSet(display, fontset);
114                    xlib::XCloseDisplay(display);
115                }
116                Data::XFont { display, xfont } => {
117                    xlib::XFreeFont(display, xfont);
118                    xlib::XCloseDisplay(display);
119                }
120            }
121        }
122    }
123}
124
125/// Get the width of the text rendered with the font specified by the context
126pub fn get_text_width<S: AsRef<str>>(ctx: &Context, text: S) -> Result<u64, XError> {
127    let text = CString::new(text.as_ref())?;
128    unsafe {
129        match ctx.data {
130            Data::FontSet { fontset, .. } => {
131                let mut rectangle = MaybeUninit::uninit();
132                xlib::XmbTextExtents(
133                    fontset,
134                    text.as_ptr(),
135                    text.as_bytes().len() as i32,
136                    ptr::null_mut(),
137                    rectangle.as_mut_ptr(),
138                );
139                Ok(rectangle.assume_init().width as u64)
140            }
141            Data::XFont { xfont, .. } => {
142                Ok(xlib::XTextWidth(xfont, text.as_ptr(), text.as_bytes().len() as i32) as u64)
143            }
144        }
145    }
146}
147
148/// Sets up xlib to be multithreaded
149///
150/// Make sure you call this before doing __anything__ else xlib related.
151/// Also, do not call this more than once preferably
152pub fn setup_multithreading() {
153    unsafe {
154        xlib::XInitThreads();
155    }
156}
157
158#[cfg(test)]
159mod test {
160    use super::{get_text_width, Context};
161    use std::sync::Once;
162    use x11::xlib;
163    static SETUP: Once = Once::new();
164    // THIS MUST BE CALLED AT THE BEGINNING OF EACH TEST TO MAKE SURE THAT IT IS THREAD-SAFE!!!
165    fn setup() {
166        SETUP.call_once(|| unsafe {
167            xlib::XInitThreads();
168        })
169    }
170    #[test]
171    fn test_context_new() {
172        setup();
173        let ctx = Context::with_misc();
174        assert!(ctx.is_ok());
175    }
176    #[test]
177    fn test_context_drop() {
178        setup();
179        let ctx = Context::with_misc();
180        drop(ctx);
181        assert!(true);
182    }
183    #[test]
184    fn test_text_width() {
185        setup();
186        let ctx = Context::with_misc().unwrap();
187        assert!(get_text_width(&ctx, "Hello World").unwrap() > 0);
188    }
189    #[test]
190    fn test_text_alternate() {
191        setup();
192        let ctx = Context::new("?");
193        assert!(ctx.is_err());
194    }
195}