mod app_menu;
#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
mod threaded_dispatcher;
#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
mod test;
#[cfg(all(target_os = "macos", any(test, feature = "test-support")))]
mod visual_test;
use crate::{App, AsyncWindowContext, Bounds, BoundsExt, ClipboardItem, Image, ImageFormat, ImageSource, Pixels, PlatformInputHandler, PlatformInputHandlerDelegate, Point, RenderImage, Size, SvgRenderer, TextInputConfiguration, UTF16Selection, Window, WindowBounds};
use anyhow::{Context as _, Result};
use image::codecs::gif::GifDecoder;
use image::{AnimationDecoder as _, DynamicImage, Frame};
use smallvec::SmallVec;
use std::io::Cursor;
use std::{ops::Range, sync::Arc};
pub use app_menu::*;
#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
pub(crate) use test::*;
#[cfg(any(test, feature = "test-support"))]
pub use test::{TestScreenCaptureSource, TestScreenCaptureStream};
#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
pub use threaded_dispatcher::{PlatformDispatcherExt, ThreadedDispatcher};
#[cfg(all(target_os = "macos", any(test, feature = "test-support")))]
pub use visual_test::VisualTestPlatform;
#[doc(hidden)]
pub enum TasksIncluded {
OnlyCompleted,
CompletedAndRunning,
}
struct GpuiInputHandler {
cx: AsyncWindowContext,
handler: Box<dyn InputHandler>,
}
impl PlatformInputHandlerDelegate for GpuiInputHandler {
fn selected_text_range(&mut self, ignore_disabled_input: bool) -> Option<UTF16Selection> {
self.cx
.update(|window, cx| {
self.handler
.selected_text_range(ignore_disabled_input, window, cx)
})
.ok()
.flatten()
}
fn marked_text_range(&mut self) -> Option<Range<usize>> {
self.cx
.update(|window, cx| self.handler.marked_text_range(window, cx))
.ok()
.flatten()
}
fn text_for_range(
&mut self,
range_utf16: Range<usize>,
adjusted: &mut Option<Range<usize>>,
) -> Option<String> {
self.cx
.update(|window, cx| {
self.handler
.text_for_range(range_utf16, adjusted, window, cx)
})
.ok()
.flatten()
}
fn replace_text_in_range(&mut self, replacement_range: Option<Range<usize>>, text: &str) {
self.cx
.update(|window, cx| {
self.handler
.replace_text_in_range(replacement_range, text, window, cx);
})
.ok();
}
fn replace_and_mark_text_in_range(
&mut self,
range_utf16: Option<Range<usize>>,
new_text: &str,
new_selected_range: Option<Range<usize>>,
) {
self.cx
.update(|window, cx| {
self.handler.replace_and_mark_text_in_range(
range_utf16,
new_text,
new_selected_range,
window,
cx,
)
})
.ok();
}
fn unmark_text(&mut self) {
self.cx
.update(|window, cx| self.handler.unmark_text(window, cx))
.ok();
}
fn paste(&mut self, item: ClipboardItem) {
self.cx
.update(|window, cx| self.handler.paste(item, window, cx))
.ok();
}
fn bounds_for_range(&mut self, range_utf16: Range<usize>) -> Option<Bounds<Pixels>> {
self.cx
.update(|window, cx| self.handler.bounds_for_range(range_utf16, window, cx))
.ok()
.flatten()
}
fn apple_press_and_hold_enabled(&mut self) -> bool {
self.handler.apple_press_and_hold_enabled()
}
fn character_index_for_point(&mut self, point: Point<Pixels>) -> Option<usize> {
self.cx
.update(|window, cx| self.handler.character_index_for_point(point, window, cx))
.ok()
.flatten()
}
fn set_selected_text_range(&mut self, range_utf16: Range<usize>) {
self.cx
.update(|window, cx| {
self.handler
.set_selected_text_range(range_utf16, window, cx)
})
.ok();
}
fn element_bounds(&mut self) -> Option<Bounds<Pixels>> {
self.cx
.update(|window, cx| self.handler.element_bounds(window, cx))
.ok()
.flatten()
}
fn text_length_utf16(&mut self) -> Option<usize> {
self.cx
.update(|window, cx| self.handler.text_length_utf16(window, cx))
.ok()
.flatten()
}
fn query_accepts_text_input(&mut self) -> bool {
self.cx
.update(|window, cx| self.handler.accepts_text_input(window, cx))
.unwrap_or(true)
}
fn query_prefers_ime_for_printable_keys(&mut self) -> bool {
self.cx
.update(|window, cx| {
!window.has_pending_keystrokes()
&& self.handler.prefers_ime_for_printable_keys(window, cx)
})
.unwrap_or(false)
}
fn text_input_editable_range(&mut self) -> Option<Range<usize>> {
self.cx
.update(|window, cx| self.handler.text_input_editable_range(window, cx))
.ok()
.flatten()
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
}
pub fn new_platform_input_handler(
cx: AsyncWindowContext,
handler: Box<dyn InputHandler>,
) -> PlatformInputHandler {
PlatformInputHandler::from_delegate(Box::new(GpuiInputHandler { cx, handler }))
}
pub trait PlatformInputHandlerExt {
fn dispatch_input(&mut self, input: &str, window: &mut Window, cx: &mut App);
fn selected_bounds(&mut self, window: &mut Window, cx: &mut App) -> Option<Bounds<Pixels>>;
fn accepts_text_input(&mut self, window: &mut Window, cx: &mut App) -> bool;
fn text_input_configuration(
&mut self,
window: &mut Window,
cx: &mut App,
) -> TextInputConfiguration;
}
impl PlatformInputHandlerExt for PlatformInputHandler {
fn dispatch_input(&mut self, input: &str, window: &mut Window, cx: &mut App) {
if let Some(delegate) = self
.delegate_as_any_mut()
.downcast_mut::<GpuiInputHandler>()
{
delegate
.handler
.replace_text_in_range(None, input, window, cx);
}
}
fn selected_bounds(&mut self, window: &mut Window, cx: &mut App) -> Option<Bounds<Pixels>> {
let delegate = self
.delegate_as_any_mut()
.downcast_mut::<GpuiInputHandler>()?;
let marked_range = delegate.handler.marked_text_range(window, cx);
let selection = delegate.handler.selected_text_range(true, window, cx)?;
PlatformInputHandler::compute_ime_candidate_bounds(marked_range, &selection, |range| {
delegate.handler.bounds_for_range(range, window, cx)
})
}
fn accepts_text_input(&mut self, window: &mut Window, cx: &mut App) -> bool {
self.delegate_as_any_mut()
.downcast_mut::<GpuiInputHandler>()
.map(|delegate| delegate.handler.accepts_text_input(window, cx))
.unwrap_or(false)
}
fn text_input_configuration(
&mut self,
window: &mut Window,
cx: &mut App,
) -> TextInputConfiguration {
self.delegate_as_any_mut()
.downcast_mut::<GpuiInputHandler>()
.map(|delegate| delegate.handler.text_input_configuration(window, cx))
.unwrap_or_default()
}
}
pub trait InputHandler: 'static {
fn selected_text_range(
&mut self,
ignore_disabled_input: bool,
window: &mut Window,
cx: &mut App,
) -> Option<UTF16Selection>;
fn marked_text_range(&mut self, window: &mut Window, cx: &mut App) -> Option<Range<usize>>;
fn text_for_range(
&mut self,
range_utf16: Range<usize>,
adjusted_range: &mut Option<Range<usize>>,
window: &mut Window,
cx: &mut App,
) -> Option<String>;
fn replace_text_in_range(
&mut self,
replacement_range: Option<Range<usize>>,
text: &str,
window: &mut Window,
cx: &mut App,
);
fn replace_and_mark_text_in_range(
&mut self,
range_utf16: Option<Range<usize>>,
new_text: &str,
new_selected_range: Option<Range<usize>>,
window: &mut Window,
cx: &mut App,
);
fn unmark_text(&mut self, window: &mut Window, cx: &mut App);
fn paste(&mut self, item: ClipboardItem, window: &mut Window, cx: &mut App) {
if let Some(text) = item.text() {
self.replace_text_in_range(None, &text, window, cx);
}
}
fn bounds_for_range(
&mut self,
range_utf16: Range<usize>,
window: &mut Window,
cx: &mut App,
) -> Option<Bounds<Pixels>>;
fn character_index_for_point(
&mut self,
point: Point<Pixels>,
window: &mut Window,
cx: &mut App,
) -> Option<usize>;
fn set_selected_text_range(
&mut self,
_range_utf16: Range<usize>,
_window: &mut Window,
_cx: &mut App,
) {
}
fn element_bounds(&mut self, _window: &mut Window, _cx: &mut App) -> Option<Bounds<Pixels>> {
None
}
fn text_length_utf16(&mut self, _window: &mut Window, _cx: &mut App) -> Option<usize> {
None
}
#[allow(dead_code)]
fn apple_press_and_hold_enabled(&mut self) -> bool {
true
}
fn accepts_text_input(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
true
}
fn text_input_editable_range(
&mut self,
_window: &mut Window,
_cx: &mut App,
) -> Option<Range<usize>> {
None
}
fn prefers_ime_for_printable_keys(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
false
}
fn text_input_configuration(
&mut self,
_window: &mut Window,
_cx: &mut App,
) -> TextInputConfiguration {
TextInputConfiguration::default()
}
}
pub trait WindowBoundsExt: Sized {
fn centered(size: Size<Pixels>, cx: &App) -> Self;
}
impl WindowBoundsExt for WindowBounds {
fn centered(size: Size<Pixels>, cx: &App) -> Self {
WindowBounds::Windowed(Bounds::centered(None, size, cx))
}
}
pub(crate) fn decode_static_image(
bytes: &[u8],
format: image::ImageFormat,
) -> Result<SmallVec<[Frame; 1]>> {
let decoder = image::ImageReader::with_format(Cursor::new(bytes), format)
.into_decoder()
.context("creating image decoder")?;
decode_static_image_from_decoder(decoder)
}
pub(crate) fn decode_static_image_from_decoder(
mut decoder: impl image::ImageDecoder,
) -> Result<SmallVec<[Frame; 1]>> {
let orientation = decoder
.orientation()
.context("reading decoder's orientation")?;
let mut image = DynamicImage::from_decoder(decoder).context("decoding image")?;
image.apply_orientation(orientation);
let mut data = image.into_rgba8();
for pixel in data.chunks_exact_mut(4) {
pixel.swap(0, 2);
}
Ok(SmallVec::from_elem(Frame::new(data), 1))
}
pub trait ImageExt: Sized {
fn use_render_image(
self: Arc<Self>,
window: &mut Window,
cx: &mut App,
) -> Option<Arc<RenderImage>>;
fn get_render_image(
self: Arc<Self>,
window: &mut Window,
cx: &mut App,
) -> Option<Arc<RenderImage>>;
fn remove_asset(self: Arc<Self>, cx: &mut App);
#[cfg(any(test, feature = "test-support"))]
fn is_asset_cached(self: &Arc<Self>, cx: &App) -> bool;
fn to_image_data(&self, svg_renderer: SvgRenderer) -> Result<Arc<RenderImage>>;
}
impl ImageExt for Image {
fn use_render_image(
self: Arc<Self>,
window: &mut Window,
cx: &mut App,
) -> Option<Arc<RenderImage>> {
ImageSource::Image(self)
.use_data(None, window, cx)
.and_then(|result| result.ok())
}
fn get_render_image(
self: Arc<Self>,
window: &mut Window,
cx: &mut App,
) -> Option<Arc<RenderImage>> {
ImageSource::Image(self)
.get_data(None, window, cx)
.and_then(|result| result.ok())
}
fn remove_asset(self: Arc<Self>, cx: &mut App) {
ImageSource::Image(self).remove_asset(cx);
}
#[cfg(any(test, feature = "test-support"))]
fn is_asset_cached(self: &Arc<Self>, cx: &App) -> bool {
ImageSource::Image(self.clone()).is_asset_cached(cx)
}
fn to_image_data(&self, svg_renderer: SvgRenderer) -> Result<Arc<RenderImage>> {
let frames = match self.format {
ImageFormat::Gif => {
let decoder = GifDecoder::new(Cursor::new(&self.bytes))?;
let mut frames = SmallVec::new();
for frame in decoder.into_frames() {
match frame {
Ok(mut frame) => {
for pixel in frame.buffer_mut().chunks_exact_mut(4) {
pixel.swap(0, 2);
}
frames.push(frame);
}
Err(err) => {
log::debug!("Skipping GIF frame due to decode error: {err}");
}
}
}
if frames.is_empty() {
anyhow::bail!("GIF could not be decoded: all frames failed");
}
frames
}
ImageFormat::Png => decode_static_image(&self.bytes, image::ImageFormat::Png)?,
ImageFormat::Jpeg => decode_static_image(&self.bytes, image::ImageFormat::Jpeg)?,
ImageFormat::Webp => decode_static_image(&self.bytes, image::ImageFormat::WebP)?,
ImageFormat::Bmp => decode_static_image(&self.bytes, image::ImageFormat::Bmp)?,
ImageFormat::Tiff => decode_static_image(&self.bytes, image::ImageFormat::Tiff)?,
ImageFormat::Ico => decode_static_image(&self.bytes, image::ImageFormat::Ico)?,
ImageFormat::Svg => {
return svg_renderer
.render_single_frame(&self.bytes, 1.0)
.map_err(Into::into);
}
ImageFormat::Pnm => decode_static_image(&self.bytes, image::ImageFormat::Pnm)?,
};
Ok(Arc::new(RenderImage::new(frames)))
}
}
#[cfg(test)]
mod image_tests {
use super::*;
use crate::size;
use std::sync::Arc;
#[test]
fn test_image_to_image_data_applies_exif_orientation() {
let image = Image::from_bytes(
ImageFormat::Jpeg,
include_bytes!("../../gpui/examples/image/exif-orientation-rotate-180.jpg").to_vec(),
);
let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap();
assert_eq!(render_image.size(0), size(16.into(), 32.into()));
let bytes = render_image.as_bytes(0).unwrap();
assert_eq!(&bytes[..4], &[255, 255, 255, 255]);
assert_eq!(&bytes[(16 * 32 - 1) * 4..], &[0, 0, 0, 255]);
}
#[test]
fn test_svg_image_to_image_data_converts_to_bgra() {
let image = Image::from_bytes(
ImageFormat::Svg,
br##"<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1">
<rect width="1" height="1" fill="#38BDF8"/>
</svg>"##
.to_vec(),
);
let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap();
let bytes = render_image.as_bytes(0).unwrap();
for pixel in bytes.chunks_exact(4) {
assert_eq!(pixel, &[0xF8, 0xBD, 0x38, 0xFF]);
}
}
}
#[cfg(all(test, any(target_os = "linux", target_os = "freebsd")))]
mod tests {
use crate::{WindowButton, WindowButtonLayout};
use std::collections::HashSet;
#[test]
fn test_window_button_layout_parse_standard() {
let layout = WindowButtonLayout::parse("close,minimize:maximize").unwrap();
assert_eq!(
layout.left,
[
Some(WindowButton::Close),
Some(WindowButton::Minimize),
None
]
);
assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
}
#[test]
fn test_window_button_layout_parse_right_only() {
let layout = WindowButtonLayout::parse("minimize,maximize,close").unwrap();
assert_eq!(layout.left, [None, None, None]);
assert_eq!(
layout.right,
[
Some(WindowButton::Minimize),
Some(WindowButton::Maximize),
Some(WindowButton::Close)
]
);
}
#[test]
fn test_window_button_layout_parse_left_only() {
let layout = WindowButtonLayout::parse("close,minimize,maximize:").unwrap();
assert_eq!(
layout.left,
[
Some(WindowButton::Close),
Some(WindowButton::Minimize),
Some(WindowButton::Maximize)
]
);
assert_eq!(layout.right, [None, None, None]);
}
#[test]
fn test_window_button_layout_parse_with_whitespace() {
let layout = WindowButtonLayout::parse(" close , minimize : maximize ").unwrap();
assert_eq!(
layout.left,
[
Some(WindowButton::Close),
Some(WindowButton::Minimize),
None
]
);
assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
}
#[test]
fn test_window_button_layout_parse_empty() {
let layout = WindowButtonLayout::parse("").unwrap();
assert_eq!(layout.left, [None, None, None]);
assert_eq!(layout.right, [None, None, None]);
}
#[test]
fn test_window_button_layout_parse_intentionally_empty() {
let layout = WindowButtonLayout::parse(":").unwrap();
assert_eq!(layout.left, [None, None, None]);
assert_eq!(layout.right, [None, None, None]);
}
#[test]
fn test_window_button_layout_parse_invalid_buttons() {
let layout = WindowButtonLayout::parse("close,invalid,minimize:maximize,foo").unwrap();
assert_eq!(
layout.left,
[
Some(WindowButton::Close),
Some(WindowButton::Minimize),
None
]
);
assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
}
#[test]
fn test_window_button_layout_parse_deduplicates_same_side_buttons() {
let layout = WindowButtonLayout::parse("close,close,minimize").unwrap();
assert_eq!(
layout.right,
[
Some(WindowButton::Close),
Some(WindowButton::Minimize),
None
]
);
assert_eq!(layout.format(), ":close,minimize");
}
#[test]
fn test_window_button_layout_parse_deduplicates_buttons_across_sides() {
let layout = WindowButtonLayout::parse("close:maximize,close,minimize").unwrap();
assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
assert_eq!(
layout.right,
[
Some(WindowButton::Maximize),
Some(WindowButton::Minimize),
None
]
);
let button_ids: Vec<_> = layout
.left
.iter()
.chain(layout.right.iter())
.flatten()
.map(WindowButton::id)
.collect();
let unique_button_ids = button_ids.iter().copied().collect::<HashSet<_>>();
assert_eq!(unique_button_ids.len(), button_ids.len());
assert_eq!(layout.format(), "close:maximize,minimize");
}
#[test]
fn test_window_button_layout_parse_gnome_style() {
let layout = WindowButtonLayout::parse("close").unwrap();
assert_eq!(layout.left, [None, None, None]);
assert_eq!(layout.right, [Some(WindowButton::Close), None, None]);
}
#[test]
fn test_window_button_layout_parse_elementary_style() {
let layout = WindowButtonLayout::parse("close:maximize").unwrap();
assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
}
#[test]
fn test_window_button_layout_round_trip() {
let cases = [
"close:minimize,maximize",
"minimize,maximize,close:",
":close",
"close:",
"close:maximize",
":",
];
for case in cases {
let layout = WindowButtonLayout::parse(case).unwrap();
assert_eq!(layout.format(), case, "Round-trip failed for: {}", case);
}
}
#[test]
fn test_window_button_layout_linux_default() {
let layout = WindowButtonLayout::linux_default();
assert_eq!(layout.left, [None, None, None]);
assert_eq!(
layout.right,
[
Some(WindowButton::Minimize),
Some(WindowButton::Maximize),
Some(WindowButton::Close)
]
);
let round_tripped = WindowButtonLayout::parse(&layout.format()).unwrap();
assert_eq!(round_tripped, layout);
}
#[test]
fn test_window_button_layout_parse_all_invalid() {
assert!(WindowButtonLayout::parse("asdfghjkl").is_err());
}
}