1mod app_menu;
2
3#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
7mod threaded_dispatcher;
8
9#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
10mod test;
11
12#[cfg(all(target_os = "macos", any(test, feature = "test-support")))]
13mod visual_test;
14
15use crate::{App, AsyncWindowContext, Bounds, BoundsExt, ClipboardItem, Image, ImageFormat, ImageSource, Pixels, PlatformInputHandler, PlatformInputHandlerDelegate, Point, RenderImage, Size, SvgRenderer, TextInputConfiguration, UTF16Selection, Window, WindowBounds};
16use anyhow::{Context as _, Result};
17use image::codecs::gif::GifDecoder;
18use image::{AnimationDecoder as _, DynamicImage, Frame};
19use smallvec::SmallVec;
20use std::io::Cursor;
21use std::{ops::Range, sync::Arc};
22
23pub use app_menu::*;
24
25#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
26pub(crate) use test::*;
27
28#[cfg(any(test, feature = "test-support"))]
29pub use test::{TestScreenCaptureSource, TestScreenCaptureStream};
30
31#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
32pub use threaded_dispatcher::{PlatformDispatcherExt, ThreadedDispatcher};
33
34#[cfg(all(target_os = "macos", any(test, feature = "test-support")))]
35pub use visual_test::VisualTestPlatform;
36
37#[doc(hidden)]
38pub enum TasksIncluded {
39 OnlyCompleted,
40 CompletedAndRunning,
41}
42
43struct GpuiInputHandler {
45 cx: AsyncWindowContext,
46 handler: Box<dyn InputHandler>,
47}
48
49impl PlatformInputHandlerDelegate for GpuiInputHandler {
50 fn selected_text_range(&mut self, ignore_disabled_input: bool) -> Option<UTF16Selection> {
51 self.cx
52 .update(|window, cx| {
53 self.handler
54 .selected_text_range(ignore_disabled_input, window, cx)
55 })
56 .ok()
57 .flatten()
58 }
59
60 fn marked_text_range(&mut self) -> Option<Range<usize>> {
61 self.cx
62 .update(|window, cx| self.handler.marked_text_range(window, cx))
63 .ok()
64 .flatten()
65 }
66
67 fn text_for_range(
68 &mut self,
69 range_utf16: Range<usize>,
70 adjusted: &mut Option<Range<usize>>,
71 ) -> Option<String> {
72 self.cx
73 .update(|window, cx| {
74 self.handler
75 .text_for_range(range_utf16, adjusted, window, cx)
76 })
77 .ok()
78 .flatten()
79 }
80
81 fn replace_text_in_range(&mut self, replacement_range: Option<Range<usize>>, text: &str) {
82 self.cx
83 .update(|window, cx| {
84 self.handler
85 .replace_text_in_range(replacement_range, text, window, cx);
86 })
87 .ok();
88 }
89
90 fn replace_and_mark_text_in_range(
91 &mut self,
92 range_utf16: Option<Range<usize>>,
93 new_text: &str,
94 new_selected_range: Option<Range<usize>>,
95 ) {
96 self.cx
97 .update(|window, cx| {
98 self.handler.replace_and_mark_text_in_range(
99 range_utf16,
100 new_text,
101 new_selected_range,
102 window,
103 cx,
104 )
105 })
106 .ok();
107 }
108
109 fn unmark_text(&mut self) {
110 self.cx
111 .update(|window, cx| self.handler.unmark_text(window, cx))
112 .ok();
113 }
114
115 fn paste(&mut self, item: ClipboardItem) {
116 self.cx
117 .update(|window, cx| self.handler.paste(item, window, cx))
118 .ok();
119 }
120
121 fn bounds_for_range(&mut self, range_utf16: Range<usize>) -> Option<Bounds<Pixels>> {
122 self.cx
123 .update(|window, cx| self.handler.bounds_for_range(range_utf16, window, cx))
124 .ok()
125 .flatten()
126 }
127
128 fn apple_press_and_hold_enabled(&mut self) -> bool {
129 self.handler.apple_press_and_hold_enabled()
130 }
131
132 fn character_index_for_point(&mut self, point: Point<Pixels>) -> Option<usize> {
133 self.cx
134 .update(|window, cx| self.handler.character_index_for_point(point, window, cx))
135 .ok()
136 .flatten()
137 }
138
139 fn set_selected_text_range(&mut self, range_utf16: Range<usize>) {
140 self.cx
141 .update(|window, cx| {
142 self.handler
143 .set_selected_text_range(range_utf16, window, cx)
144 })
145 .ok();
146 }
147
148 fn element_bounds(&mut self) -> Option<Bounds<Pixels>> {
149 self.cx
150 .update(|window, cx| self.handler.element_bounds(window, cx))
151 .ok()
152 .flatten()
153 }
154
155 fn text_length_utf16(&mut self) -> Option<usize> {
156 self.cx
157 .update(|window, cx| self.handler.text_length_utf16(window, cx))
158 .ok()
159 .flatten()
160 }
161
162 fn query_accepts_text_input(&mut self) -> bool {
163 self.cx
164 .update(|window, cx| self.handler.accepts_text_input(window, cx))
165 .unwrap_or(true)
166 }
167
168 fn query_prefers_ime_for_printable_keys(&mut self) -> bool {
169 self.cx
170 .update(|window, cx| {
171 !window.has_pending_keystrokes()
173 && self.handler.prefers_ime_for_printable_keys(window, cx)
174 })
175 .unwrap_or(false)
176 }
177
178 fn text_input_editable_range(&mut self) -> Option<Range<usize>> {
179 self.cx
180 .update(|window, cx| self.handler.text_input_editable_range(window, cx))
181 .ok()
182 .flatten()
183 }
184
185 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
186 self
187 }
188}
189
190pub fn new_platform_input_handler(
192 cx: AsyncWindowContext,
193 handler: Box<dyn InputHandler>,
194) -> PlatformInputHandler {
195 PlatformInputHandler::from_delegate(Box::new(GpuiInputHandler { cx, handler }))
196}
197
198pub trait PlatformInputHandlerExt {
200 fn dispatch_input(&mut self, input: &str, window: &mut Window, cx: &mut App);
202 fn selected_bounds(&mut self, window: &mut Window, cx: &mut App) -> Option<Bounds<Pixels>>;
204 fn accepts_text_input(&mut self, window: &mut Window, cx: &mut App) -> bool;
206 fn text_input_configuration(
208 &mut self,
209 window: &mut Window,
210 cx: &mut App,
211 ) -> TextInputConfiguration;
212}
213
214impl PlatformInputHandlerExt for PlatformInputHandler {
215 fn dispatch_input(&mut self, input: &str, window: &mut Window, cx: &mut App) {
216 if let Some(delegate) = self
217 .delegate_as_any_mut()
218 .downcast_mut::<GpuiInputHandler>()
219 {
220 delegate
221 .handler
222 .replace_text_in_range(None, input, window, cx);
223 }
224 }
225
226 fn selected_bounds(&mut self, window: &mut Window, cx: &mut App) -> Option<Bounds<Pixels>> {
227 let delegate = self
228 .delegate_as_any_mut()
229 .downcast_mut::<GpuiInputHandler>()?;
230 let marked_range = delegate.handler.marked_text_range(window, cx);
231 let selection = delegate.handler.selected_text_range(true, window, cx)?;
232 PlatformInputHandler::compute_ime_candidate_bounds(marked_range, &selection, |range| {
233 delegate.handler.bounds_for_range(range, window, cx)
234 })
235 }
236
237 fn accepts_text_input(&mut self, window: &mut Window, cx: &mut App) -> bool {
238 self.delegate_as_any_mut()
239 .downcast_mut::<GpuiInputHandler>()
240 .map(|delegate| delegate.handler.accepts_text_input(window, cx))
241 .unwrap_or(false)
242 }
243
244 fn text_input_configuration(
245 &mut self,
246 window: &mut Window,
247 cx: &mut App,
248 ) -> TextInputConfiguration {
249 self.delegate_as_any_mut()
250 .downcast_mut::<GpuiInputHandler>()
251 .map(|delegate| delegate.handler.text_input_configuration(window, cx))
252 .unwrap_or_default()
253 }
254}
255
256pub trait InputHandler: 'static {
261 fn selected_text_range(
266 &mut self,
267 ignore_disabled_input: bool,
268 window: &mut Window,
269 cx: &mut App,
270 ) -> Option<UTF16Selection>;
271
272 fn marked_text_range(&mut self, window: &mut Window, cx: &mut App) -> Option<Range<usize>>;
277
278 fn text_for_range(
283 &mut self,
284 range_utf16: Range<usize>,
285 adjusted_range: &mut Option<Range<usize>>,
286 window: &mut Window,
287 cx: &mut App,
288 ) -> Option<String>;
289
290 fn replace_text_in_range(
295 &mut self,
296 replacement_range: Option<Range<usize>>,
297 text: &str,
298 window: &mut Window,
299 cx: &mut App,
300 );
301
302 fn replace_and_mark_text_in_range(
309 &mut self,
310 range_utf16: Option<Range<usize>>,
311 new_text: &str,
312 new_selected_range: Option<Range<usize>>,
313 window: &mut Window,
314 cx: &mut App,
315 );
316
317 fn unmark_text(&mut self, window: &mut Window, cx: &mut App);
320
321 fn paste(&mut self, item: ClipboardItem, window: &mut Window, cx: &mut App) {
328 if let Some(text) = item.text() {
329 self.replace_text_in_range(None, &text, window, cx);
330 }
331 }
332
333 fn bounds_for_range(
338 &mut self,
339 range_utf16: Range<usize>,
340 window: &mut Window,
341 cx: &mut App,
342 ) -> Option<Bounds<Pixels>>;
343
344 fn character_index_for_point(
348 &mut self,
349 point: Point<Pixels>,
350 window: &mut Window,
351 cx: &mut App,
352 ) -> Option<usize>;
353
354 fn set_selected_text_range(
364 &mut self,
365 _range_utf16: Range<usize>,
366 _window: &mut Window,
367 _cx: &mut App,
368 ) {
369 }
370
371 fn element_bounds(&mut self, _window: &mut Window, _cx: &mut App) -> Option<Bounds<Pixels>> {
378 None
379 }
380
381 fn text_length_utf16(&mut self, _window: &mut Window, _cx: &mut App) -> Option<usize> {
383 None
384 }
385
386 #[allow(dead_code)]
391 fn apple_press_and_hold_enabled(&mut self) -> bool {
392 true
393 }
394
395 fn accepts_text_input(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
397 true
398 }
399
400 fn text_input_editable_range(
411 &mut self,
412 _window: &mut Window,
413 _cx: &mut App,
414 ) -> Option<Range<usize>> {
415 None
416 }
417
418 fn prefers_ime_for_printable_keys(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
427 false
428 }
429
430 fn text_input_configuration(
436 &mut self,
437 _window: &mut Window,
438 _cx: &mut App,
439 ) -> TextInputConfiguration {
440 TextInputConfiguration::default()
441 }
442}
443
444pub trait WindowBoundsExt: Sized {
446 fn centered(size: Size<Pixels>, cx: &App) -> Self;
448}
449
450impl WindowBoundsExt for WindowBounds {
451 fn centered(size: Size<Pixels>, cx: &App) -> Self {
452 WindowBounds::Windowed(Bounds::centered(None, size, cx))
453 }
454}
455
456pub(crate) fn decode_static_image(
457 bytes: &[u8],
458 format: image::ImageFormat,
459) -> Result<SmallVec<[Frame; 1]>> {
460 let decoder = image::ImageReader::with_format(Cursor::new(bytes), format)
461 .into_decoder()
462 .context("creating image decoder")?;
463 decode_static_image_from_decoder(decoder)
464}
465
466pub(crate) fn decode_static_image_from_decoder(
467 mut decoder: impl image::ImageDecoder,
468) -> Result<SmallVec<[Frame; 1]>> {
469 let orientation = decoder
470 .orientation()
471 .context("reading decoder's orientation")?;
472 let mut image = DynamicImage::from_decoder(decoder).context("decoding image")?;
473 image.apply_orientation(orientation);
474
475 let mut data = image.into_rgba8();
476 for pixel in data.chunks_exact_mut(4) {
477 pixel.swap(0, 2);
478 }
479
480 Ok(SmallVec::from_elem(Frame::new(data), 1))
481}
482
483pub trait ImageExt: Sized {
489 fn use_render_image(
491 self: Arc<Self>,
492 window: &mut Window,
493 cx: &mut App,
494 ) -> Option<Arc<RenderImage>>;
495
496 fn get_render_image(
498 self: Arc<Self>,
499 window: &mut Window,
500 cx: &mut App,
501 ) -> Option<Arc<RenderImage>>;
502
503 fn remove_asset(self: Arc<Self>, cx: &mut App);
505
506 #[cfg(any(test, feature = "test-support"))]
509 fn is_asset_cached(self: &Arc<Self>, cx: &App) -> bool;
510
511 fn to_image_data(&self, svg_renderer: SvgRenderer) -> Result<Arc<RenderImage>>;
513}
514
515impl ImageExt for Image {
516 fn use_render_image(
517 self: Arc<Self>,
518 window: &mut Window,
519 cx: &mut App,
520 ) -> Option<Arc<RenderImage>> {
521 ImageSource::Image(self)
522 .use_data(None, window, cx)
523 .and_then(|result| result.ok())
524 }
525
526 fn get_render_image(
527 self: Arc<Self>,
528 window: &mut Window,
529 cx: &mut App,
530 ) -> Option<Arc<RenderImage>> {
531 ImageSource::Image(self)
532 .get_data(None, window, cx)
533 .and_then(|result| result.ok())
534 }
535
536 fn remove_asset(self: Arc<Self>, cx: &mut App) {
537 ImageSource::Image(self).remove_asset(cx);
538 }
539
540 #[cfg(any(test, feature = "test-support"))]
541 fn is_asset_cached(self: &Arc<Self>, cx: &App) -> bool {
542 ImageSource::Image(self.clone()).is_asset_cached(cx)
543 }
544
545 fn to_image_data(&self, svg_renderer: SvgRenderer) -> Result<Arc<RenderImage>> {
546 let frames = match self.format {
547 ImageFormat::Gif => {
548 let decoder = GifDecoder::new(Cursor::new(&self.bytes))?;
549 let mut frames = SmallVec::new();
550
551 for frame in decoder.into_frames() {
552 match frame {
553 Ok(mut frame) => {
554 for pixel in frame.buffer_mut().chunks_exact_mut(4) {
556 pixel.swap(0, 2);
557 }
558 frames.push(frame);
559 }
560 Err(err) => {
561 log::debug!("Skipping GIF frame due to decode error: {err}");
562 }
563 }
564 }
565
566 if frames.is_empty() {
567 anyhow::bail!("GIF could not be decoded: all frames failed");
568 }
569
570 frames
571 }
572 ImageFormat::Png => decode_static_image(&self.bytes, image::ImageFormat::Png)?,
573 ImageFormat::Jpeg => decode_static_image(&self.bytes, image::ImageFormat::Jpeg)?,
574 ImageFormat::Webp => decode_static_image(&self.bytes, image::ImageFormat::WebP)?,
575 ImageFormat::Bmp => decode_static_image(&self.bytes, image::ImageFormat::Bmp)?,
576 ImageFormat::Tiff => decode_static_image(&self.bytes, image::ImageFormat::Tiff)?,
577 ImageFormat::Ico => decode_static_image(&self.bytes, image::ImageFormat::Ico)?,
578 ImageFormat::Svg => {
579 return svg_renderer
580 .render_single_frame(&self.bytes, 1.0)
581 .map_err(Into::into);
582 }
583 ImageFormat::Pnm => decode_static_image(&self.bytes, image::ImageFormat::Pnm)?,
584 };
585
586 Ok(Arc::new(RenderImage::new(frames)))
587 }
588}
589
590#[cfg(test)]
591mod image_tests {
592 use super::*;
593 use crate::size;
594 use std::sync::Arc;
595
596 #[test]
597 fn test_image_to_image_data_applies_exif_orientation() {
598 let image = Image::from_bytes(
599 ImageFormat::Jpeg,
600 include_bytes!("../../gpui/examples/image/exif-orientation-rotate-180.jpg").to_vec(),
602 );
603
604 let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap();
605
606 assert_eq!(render_image.size(0), size(16.into(), 32.into()));
607
608 let bytes = render_image.as_bytes(0).unwrap();
609 assert_eq!(&bytes[..4], &[255, 255, 255, 255]);
610 assert_eq!(&bytes[(16 * 32 - 1) * 4..], &[0, 0, 0, 255]);
611 }
612
613 #[test]
614 fn test_svg_image_to_image_data_converts_to_bgra() {
615 let image = Image::from_bytes(
616 ImageFormat::Svg,
617 br##"<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1">
618<rect width="1" height="1" fill="#38BDF8"/>
619</svg>"##
620 .to_vec(),
621 );
622
623 let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap();
624 let bytes = render_image.as_bytes(0).unwrap();
625
626 for pixel in bytes.chunks_exact(4) {
627 assert_eq!(pixel, &[0xF8, 0xBD, 0x38, 0xFF]);
628 }
629 }
630}
631
632#[cfg(all(test, any(target_os = "linux", target_os = "freebsd")))]
633mod tests {
634 use crate::{WindowButton, WindowButtonLayout};
635 use std::collections::HashSet;
636
637 #[test]
638 fn test_window_button_layout_parse_standard() {
639 let layout = WindowButtonLayout::parse("close,minimize:maximize").unwrap();
640 assert_eq!(
641 layout.left,
642 [
643 Some(WindowButton::Close),
644 Some(WindowButton::Minimize),
645 None
646 ]
647 );
648 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
649 }
650
651 #[test]
652 fn test_window_button_layout_parse_right_only() {
653 let layout = WindowButtonLayout::parse("minimize,maximize,close").unwrap();
654 assert_eq!(layout.left, [None, None, None]);
655 assert_eq!(
656 layout.right,
657 [
658 Some(WindowButton::Minimize),
659 Some(WindowButton::Maximize),
660 Some(WindowButton::Close)
661 ]
662 );
663 }
664
665 #[test]
666 fn test_window_button_layout_parse_left_only() {
667 let layout = WindowButtonLayout::parse("close,minimize,maximize:").unwrap();
668 assert_eq!(
669 layout.left,
670 [
671 Some(WindowButton::Close),
672 Some(WindowButton::Minimize),
673 Some(WindowButton::Maximize)
674 ]
675 );
676 assert_eq!(layout.right, [None, None, None]);
677 }
678
679 #[test]
680 fn test_window_button_layout_parse_with_whitespace() {
681 let layout = WindowButtonLayout::parse(" close , minimize : maximize ").unwrap();
682 assert_eq!(
683 layout.left,
684 [
685 Some(WindowButton::Close),
686 Some(WindowButton::Minimize),
687 None
688 ]
689 );
690 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
691 }
692
693 #[test]
694 fn test_window_button_layout_parse_empty() {
695 let layout = WindowButtonLayout::parse("").unwrap();
696 assert_eq!(layout.left, [None, None, None]);
697 assert_eq!(layout.right, [None, None, None]);
698 }
699
700 #[test]
701 fn test_window_button_layout_parse_intentionally_empty() {
702 let layout = WindowButtonLayout::parse(":").unwrap();
703 assert_eq!(layout.left, [None, None, None]);
704 assert_eq!(layout.right, [None, None, None]);
705 }
706
707 #[test]
708 fn test_window_button_layout_parse_invalid_buttons() {
709 let layout = WindowButtonLayout::parse("close,invalid,minimize:maximize,foo").unwrap();
710 assert_eq!(
711 layout.left,
712 [
713 Some(WindowButton::Close),
714 Some(WindowButton::Minimize),
715 None
716 ]
717 );
718 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
719 }
720
721 #[test]
722 fn test_window_button_layout_parse_deduplicates_same_side_buttons() {
723 let layout = WindowButtonLayout::parse("close,close,minimize").unwrap();
724 assert_eq!(
725 layout.right,
726 [
727 Some(WindowButton::Close),
728 Some(WindowButton::Minimize),
729 None
730 ]
731 );
732 assert_eq!(layout.format(), ":close,minimize");
733 }
734
735 #[test]
736 fn test_window_button_layout_parse_deduplicates_buttons_across_sides() {
737 let layout = WindowButtonLayout::parse("close:maximize,close,minimize").unwrap();
738 assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
739 assert_eq!(
740 layout.right,
741 [
742 Some(WindowButton::Maximize),
743 Some(WindowButton::Minimize),
744 None
745 ]
746 );
747
748 let button_ids: Vec<_> = layout
749 .left
750 .iter()
751 .chain(layout.right.iter())
752 .flatten()
753 .map(WindowButton::id)
754 .collect();
755 let unique_button_ids = button_ids.iter().copied().collect::<HashSet<_>>();
756 assert_eq!(unique_button_ids.len(), button_ids.len());
757 assert_eq!(layout.format(), "close:maximize,minimize");
758 }
759
760 #[test]
761 fn test_window_button_layout_parse_gnome_style() {
762 let layout = WindowButtonLayout::parse("close").unwrap();
763 assert_eq!(layout.left, [None, None, None]);
764 assert_eq!(layout.right, [Some(WindowButton::Close), None, None]);
765 }
766
767 #[test]
768 fn test_window_button_layout_parse_elementary_style() {
769 let layout = WindowButtonLayout::parse("close:maximize").unwrap();
770 assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
771 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
772 }
773
774 #[test]
775 fn test_window_button_layout_round_trip() {
776 let cases = [
777 "close:minimize,maximize",
778 "minimize,maximize,close:",
779 ":close",
780 "close:",
781 "close:maximize",
782 ":",
783 ];
784
785 for case in cases {
786 let layout = WindowButtonLayout::parse(case).unwrap();
787 assert_eq!(layout.format(), case, "Round-trip failed for: {}", case);
788 }
789 }
790
791 #[test]
792 fn test_window_button_layout_linux_default() {
793 let layout = WindowButtonLayout::linux_default();
794 assert_eq!(layout.left, [None, None, None]);
795 assert_eq!(
796 layout.right,
797 [
798 Some(WindowButton::Minimize),
799 Some(WindowButton::Maximize),
800 Some(WindowButton::Close)
801 ]
802 );
803
804 let round_tripped = WindowButtonLayout::parse(&layout.format()).unwrap();
805 assert_eq!(round_tripped, layout);
806 }
807
808 #[test]
809 fn test_window_button_layout_parse_all_invalid() {
810 assert!(WindowButtonLayout::parse("asdfghjkl").is_err());
811 }
812}