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