Skip to main content

gpui_macos/
gpui_macos.rs

1#![doc = include_str!("../README.md")]
2#![cfg(target_os = "macos")]
3#![allow(unused_mut)] // False positives in platform-specific state handling
4
5mod mac;
6
7pub(crate) use gpui::*;
8pub(crate) use mac::*;
9
10use std::rc::Rc;
11
12/// Returns the default macOS platform implementation for this process.
13pub fn current_platform(headless: bool) -> Rc<dyn gpui::Platform> {
14    Rc::new(mac::MacPlatform::new(headless))
15}
16
17#[cfg(all(test, feature = "font-kit"))]
18mod tests {
19    use super::{MacTextSystem, current_platform};
20    use gpui::{TestApp, TextRun, VisualTestCapabilities, WindowTextSystem, black, font, px};
21    use std::sync::{
22        Arc,
23        atomic::{AtomicBool, Ordering},
24    };
25    use std::time::Duration;
26
27    #[test]
28    fn headless_mac_text_layout_produces_nonzero_metrics() {
29        let app = TestApp::with_platform_text_system(Arc::new(MacTextSystem::new()));
30        let text_system = app.text_system();
31        let family = text_system
32            .all_font_names()
33            .into_iter()
34            .find(|name| !name.starts_with('.'))
35            .unwrap_or_else(|| ".SystemUIFont".to_string());
36        let window_text_system = WindowTextSystem::new(text_system.clone());
37        let text = "Headless text";
38        let layout = window_text_system.layout_line(
39            text,
40            px(16.),
41            &[TextRun {
42                len: text.len(),
43                font: font(family),
44                color: black(),
45                background_color: None,
46                underline: None,
47                strikethrough: None,
48            }],
49            None,
50        );
51
52        assert!(layout.width > px(0.));
53        assert!(layout.ascent > px(0.));
54        assert!(layout.runs.iter().any(|run| !run.glyphs.is_empty()));
55    }
56
57    #[test]
58    fn background_color_boundaries_shape_consistently() {
59        use gpui::{FontRun, LineLayout, PlatformTextSystem};
60
61        let platform = Arc::new(MacTextSystem::new());
62        // Separate TextSystems prevent one path from reusing the other's global
63        // layout cache. Both calls must actually reach CoreText.
64        let line_app = TestApp::with_platform_text_system(platform.clone());
65        let text_app = TestApp::with_platform_text_system(platform.clone());
66        let line_shaper = WindowTextSystem::new(line_app.text_system().clone());
67        let text_shaper = WindowTextSystem::new(text_app.text_system().clone());
68        // `all_font_names` enumerates CoreText family-name attributes, which
69        // are not guaranteed to resolve through font-kit's family selection
70        // (name-table mismatch, or every face filtered by load_family). Pick
71        // the first family that actually yields a FontId.
72        let (font, font_id) = platform
73            .all_font_names()
74            .into_iter()
75            .filter(|name| !name.starts_with('.'))
76            .chain([".SystemUIFont".to_string()])
77            .find_map(|family| {
78                let font = font(family);
79                platform.font_id(&font).ok().map(|id| (font, id))
80            })
81            .expect("no loadable system font");
82
83        let glyphs = |layout: &LineLayout| {
84            layout
85                .runs
86                .iter()
87                .flat_map(|run| {
88                    run.glyphs.iter().map(move |glyph| {
89                        (
90                            run.font_id,
91                            glyph.id,
92                            glyph.index,
93                            glyph.position,
94                            glyph.is_emoji,
95                        )
96                    })
97                })
98                .collect::<Vec<_>>()
99        };
100
101        for (text, split) in [
102            ("fi", 1),
103            ("سلام", "س".len()),
104            ("e\u{301}x", "e\u{301}".len()),
105            ("e\u{301}x", 1),
106            ("中文🙂", "中".len()),
107        ] {
108            let plain = TextRun {
109                len: split,
110                font: font.clone(),
111                color: black(),
112                background_color: None,
113                underline: None,
114                strikethrough: None,
115            };
116            let mut highlighted = plain.clone();
117            highlighted.len = text.len() - split;
118            highlighted.background_color = Some(black());
119            let runs = [plain, highlighted];
120
121            // Preserve shape_text's existing policy: decoration boundaries are
122            // shaping boundaries, not a promise of identical unstyled glyphs.
123            let expected = platform.layout_line(
124                text,
125                px(16.),
126                &[
127                    FontRun {
128                        font_id,
129                        len: split,
130                    },
131                    FontRun {
132                        font_id,
133                        len: text.len() - split,
134                    },
135                ],
136            );
137            let line = line_shaper.shape_line(text.into(), px(16.), &runs, None);
138            let wrapped = text_shaper
139                .shape_text(text.into(), px(16.), &runs, None, None)
140                .unwrap();
141            assert_eq!(wrapped.len(), 1);
142            for layout in [&**line, wrapped[0].unwrapped_layout.as_ref()] {
143                assert_eq!(layout.len, text.len());
144                assert_eq!(layout.width, expected.width, "{text:?}");
145                assert_eq!(layout.ascent, expected.ascent);
146                assert_eq!(layout.descent, expected.descent);
147                assert!(layout.width > px(0.));
148                let actual = glyphs(layout);
149                assert!(!actual.is_empty());
150                assert_eq!(actual, glyphs(&expected), "{text:?} split at {split}");
151                assert!(actual.iter().all(|(_, _, index, _, _)| {
152                    *index < text.len() && text.is_char_boundary(*index)
153                }));
154            }
155        }
156    }
157
158    #[test]
159    #[ignore]
160    fn real_visual_context_advance_clock_works() {
161        let capabilities = VisualTestCapabilities::detect();
162        if !capabilities.real_renderer || !capabilities.screenshot_capture {
163            eprintln!("skipping: real visual renderer is not available");
164            return;
165        }
166        let cx = gpui::RealVisualTestContext::with_platform(current_platform(false));
167        let completed = Arc::new(AtomicBool::new(false));
168        let timer = cx.executor().timer(Duration::from_secs(1));
169        let completed_task = completed.clone();
170        cx.spawn(async move {
171            timer.await;
172            completed_task.store(true, Ordering::SeqCst);
173        })
174        .detach();
175
176        cx.run_until_parked();
177        assert!(!completed.load(Ordering::SeqCst));
178
179        cx.advance_clock(Duration::from_secs(1));
180        cx.run_until_parked();
181        assert!(completed.load(Ordering::SeqCst));
182    }
183}