diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index fb99f7174436d7466d5b29c44c880e709fa76bee..45fbd34d365d192d84c016c35b3d4f7eb59cdc41 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -298,3 +298,7 @@ path = "examples/uniform_list.rs" [[example]] name = "window_shadow" path = "examples/window_shadow.rs" + +[[example]] +name = "metal_view" +path = "examples/metal_view.rs" diff --git a/crates/gpui/examples/metal_view.rs b/crates/gpui/examples/metal_view.rs new file mode 100644 index 0000000000000000000000000000000000000000..a0bb098d2f443b23f077b094b41c6876f457c09a --- /dev/null +++ b/crates/gpui/examples/metal_view.rs @@ -0,0 +1,307 @@ +use gpui::{Application, *}; +use std::sync::Arc; + +#[cfg(target_os = "macos")] +use metal::{Device, MTLPrimitiveType, RenderCommandEncoderRef, RenderPipelineState, TextureRef}; + +struct MetalViewExample { + #[cfg(target_os = "macos")] + pipeline_state: Option, + #[cfg(target_os = "macos")] + device: Option, +} + +impl MetalViewExample { + fn new() -> Self { + Self { + #[cfg(target_os = "macos")] + pipeline_state: None, + #[cfg(target_os = "macos")] + device: None, + } + } + + #[cfg(target_os = "macos")] + fn setup_metal(&mut self) { + // Create Metal device + let device = Device::system_default().expect("no Metal device"); + + // Create shader library from source + let shader_source = r#" + #include + using namespace metal; + + struct VertexOut { + float4 position [[position]]; + float4 color; + }; + + vertex VertexOut vertex_main(uint vid [[vertex_id]]) { + VertexOut out; + + // Create a rectangle using two triangles + // Triangle 1: top-left, top-right, bottom-left + // Triangle 2: top-right, bottom-right, bottom-left + float2 positions[6] = { + float2(-1.0, 1.0), // top-left + float2( 1.0, 1.0), // top-right + float2(-1.0, -1.0), // bottom-left + float2( 1.0, 1.0), // top-right + float2( 1.0, -1.0), // bottom-right + float2(-1.0, -1.0), // bottom-left + }; + + out.position = float4(positions[vid], 0.0, 1.0); + // Create a gradient color based on position + out.color = float4( + (positions[vid].x + 1.0) * 0.5, // Red based on X + (positions[vid].y + 1.0) * 0.5, // Green based on Y + 0.7, // Blue constant + 1.0 // Alpha + ); + + return out; + } + + fragment float4 fragment_main(VertexOut in [[stage_in]]) { + return in.color; + } + "#; + + let library = device + .new_library_with_source(shader_source, &metal::CompileOptions::new()) + .expect("Failed to create shader library"); + + let vertex_function = library.get_function("vertex_main", None).unwrap(); + let fragment_function = library.get_function("fragment_main", None).unwrap(); + + // Create pipeline state + let pipeline_descriptor = metal::RenderPipelineDescriptor::new(); + pipeline_descriptor.set_vertex_function(Some(&vertex_function)); + pipeline_descriptor.set_fragment_function(Some(&fragment_function)); + + // Configure color attachment + let color_attachment = pipeline_descriptor + .color_attachments() + .object_at(0) + .unwrap(); + color_attachment.set_pixel_format(metal::MTLPixelFormat::BGRA8Unorm); + + // Enable blending to work with GPUI's existing content + color_attachment.set_blending_enabled(true); + color_attachment.set_source_rgb_blend_factor(metal::MTLBlendFactor::SourceAlpha); + color_attachment + .set_destination_rgb_blend_factor(metal::MTLBlendFactor::OneMinusSourceAlpha); + color_attachment.set_source_alpha_blend_factor(metal::MTLBlendFactor::One); + color_attachment + .set_destination_alpha_blend_factor(metal::MTLBlendFactor::OneMinusSourceAlpha); + + let pipeline_state = device + .new_render_pipeline_state(&pipeline_descriptor) + .expect("Failed to create pipeline state"); + + self.device = Some(device); + self.pipeline_state = Some(pipeline_state); + } + + #[cfg(target_os = "macos")] + fn create_render_callback(&self) -> MetalRenderCallback { + let pipeline_state = self.pipeline_state.clone().unwrap(); + + Arc::new( + move |encoder: &RenderCommandEncoderRef, + _target: &TextureRef, + bounds: Bounds, + scale_factor: f32| { + // Set the pipeline state + encoder.set_render_pipeline_state(&pipeline_state); + + // Set viewport to match element bounds + let viewport = metal::MTLViewport { + originX: bounds.origin.x.0 as f64 * scale_factor as f64, + originY: bounds.origin.y.0 as f64 * scale_factor as f64, + width: bounds.size.width.0 as f64 * scale_factor as f64, + height: bounds.size.height.0 as f64 * scale_factor as f64, + znear: 0.0, + zfar: 1.0, + }; + encoder.set_viewport(viewport); + + // Draw the rectangle (6 vertices for 2 triangles) + encoder.draw_primitives(MTLPrimitiveType::Triangle, 0, 6); + }, + ) + } +} + +impl Render for MetalViewExample { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + // Initialize Metal on first render if on macOS + #[cfg(target_os = "macos")] + if self.pipeline_state.is_none() { + self.setup_metal(); + } + + div() + .flex() + .bg(rgb(0x1e1e1e)) + .size_full() + .justify_center() + .items_center() + .child( + div() + .flex_col() + .gap_4() + .child( + div().flex().justify_center().child( + div() + .child("Metal View Example") + .text_xl() + .text_color(rgb(0xffffff)), + ), + ) + .child( + div() + .border_1() + .border_color(rgb(0x444444)) + .rounded_md() + .overflow_hidden() + .child( + // The Metal view + #[cfg(target_os = "macos")] + { + let callback = self.create_render_callback(); + metal_view() + .render_with_shared(callback) + .w(px(400.0)) + .h(px(300.0)) + .bg(rgb(0x000000)) + }, + #[cfg(not(target_os = "macos"))] + { + // Fallback for non-macOS platforms + div() + .w(px(400.0)) + .h(px(300.0)) + .bg(rgb(0x222222)) + .flex() + .justify_center() + .items_center() + .child( + div() + .child("Metal rendering is only available on macOS") + .text_color(rgb(0x888888)), + ) + }, + ), + ) + .child( + div().flex().justify_center().child( + div() + .child("A gradient rectangle rendered with custom Metal shaders") + .text_sm() + .text_color(rgb(0xaaaaaa)), + ), + ), + ) + } +} + +fn main() { + Application::new().run(|cx: &mut App| { + let _ = cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(Bounds { + origin: Point::new(px(100.0), px(100.0)), + size: Size { + width: px(600.0), + height: px(500.0), + }, + })), + titlebar: Some(TitlebarOptions { + title: Some("Metal View Example".into()), + ..Default::default() + }), + ..Default::default() + }, + |_window, cx| cx.new(|_cx| MetalViewExample::new()), + ); + }); +} + +// Additional example: Using MetalView for more complex rendering +#[cfg(target_os = "macos")] +#[allow(dead_code)] +mod advanced_example { + use super::*; + use std::sync::Mutex; + + /// Example of a MetalView that renders an animated scene + pub struct AnimatedMetalView { + device: Device, + pipeline_state: RenderPipelineState, + frame_count: Arc>, + } + + impl AnimatedMetalView { + pub fn create_animated_renderer(&self) -> MetalRenderCallback { + let pipeline_state = self.pipeline_state.clone(); + let frame_count = self.frame_count.clone(); + + Arc::new( + move |encoder: &RenderCommandEncoderRef, + _target: &TextureRef, + bounds: Bounds, + scale_factor: f32| { + // Update animation state + let mut count = frame_count.lock().unwrap(); + *count += 0.01; + let time = *count; + + // Set pipeline and viewport + encoder.set_render_pipeline_state(&pipeline_state); + + let viewport = metal::MTLViewport { + originX: bounds.origin.x.0 as f64 * scale_factor as f64, + originY: bounds.origin.y.0 as f64 * scale_factor as f64, + width: bounds.size.width.0 as f64 * scale_factor as f64, + height: bounds.size.height.0 as f64 * scale_factor as f64, + znear: 0.0, + zfar: 1.0, + }; + encoder.set_viewport(viewport); + + // Pass time as a uniform + encoder.set_vertex_bytes( + 0, + std::mem::size_of::() as u64, + &time as *const f32 as *const _, + ); + + // Draw animated geometry + encoder.draw_primitives(MTLPrimitiveType::TriangleStrip, 0, 4); + }, + ) + } + } +} + +// Example usage in a component: +// ```rust +// struct MyApp { +// metal_renderer: Option, +// } +// +// impl Render for MyApp { +// fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { +// div() +// .child( +// metal_view() +// .render_with(|encoder, target, bounds, scale_factor| { +// // Your custom Metal rendering code here +// }) +// .size_full() +// ) +// } +// } +// ``` diff --git a/crates/gpui/examples/metal_view_quad.rs b/crates/gpui/examples/metal_view_quad.rs new file mode 100644 index 0000000000000000000000000000000000000000..a1c5d85b90642a9d2348f6f91334f8d2aabdf369 --- /dev/null +++ b/crates/gpui/examples/metal_view_quad.rs @@ -0,0 +1,355 @@ +use gpui::{prelude::*, *}; +use std::sync::Arc; + +#[cfg(target_os = "macos")] +use metal::{Device, MTLPrimitiveType, RenderCommandEncoderRef, RenderPipelineState, TextureRef}; + +struct MetalQuadExample { + #[cfg(target_os = "macos")] + pipeline_state: Option, + #[cfg(target_os = "macos")] + device: Option, +} + +impl MetalQuadExample { + fn new() -> Self { + Self { + #[cfg(target_os = "macos")] + pipeline_state: None, + #[cfg(target_os = "macos")] + device: None, + } + } + + #[cfg(target_os = "macos")] + fn setup_metal(&mut self) { + let device = Device::system_default().expect("no Metal device"); + + // Shader that properly handles viewport transformation + let shader_source = r#" + #include + using namespace metal; + + struct Uniforms { + float2 viewport_size; + }; + + struct VertexOut { + float4 position [[position]]; + float4 color; + }; + + vertex VertexOut vertex_main( + uint vid [[vertex_id]], + constant Uniforms& uniforms [[buffer(0)]] + ) { + VertexOut out; + + // Define a quad in pixel coordinates (0,0 to viewport_size) + float2 positions[6] = { + float2(0.0, 0.0), // top-left + float2(uniforms.viewport_size.x, 0.0), // top-right + float2(0.0, uniforms.viewport_size.y), // bottom-left + float2(uniforms.viewport_size.x, 0.0), // top-right + float2(uniforms.viewport_size.x, uniforms.viewport_size.y), // bottom-right + float2(0.0, uniforms.viewport_size.y), // bottom-left + }; + + // Transform from pixel coordinates to normalized device coordinates + float2 pos = positions[vid]; + float2 ndc = (pos / uniforms.viewport_size) * 2.0 - 1.0; + ndc.y = -ndc.y; // Flip Y axis to match screen coordinates + + out.position = float4(ndc, 0.0, 1.0); + + // Create a nice gradient + float2 uv = pos / uniforms.viewport_size; + out.color = float4( + uv.x, // Red increases left to right + uv.y, // Green increases top to bottom + 1.0 - uv.x, // Blue decreases left to right + 1.0 // Full opacity + ); + + return out; + } + + fragment float4 fragment_main(VertexOut in [[stage_in]]) { + return in.color; + } + "#; + + let library = device + .new_library_with_source(shader_source, &metal::CompileOptions::new()) + .expect("Failed to create shader library"); + + let vertex_function = library.get_function("vertex_main", None).unwrap(); + let fragment_function = library.get_function("fragment_main", None).unwrap(); + + // Create pipeline state + let pipeline_descriptor = metal::RenderPipelineDescriptor::new(); + pipeline_descriptor.set_vertex_function(Some(&vertex_function)); + pipeline_descriptor.set_fragment_function(Some(&fragment_function)); + + let color_attachment = pipeline_descriptor + .color_attachments() + .object_at(0) + .unwrap(); + color_attachment.set_pixel_format(metal::MTLPixelFormat::BGRA8Unorm); + + // Enable blending + color_attachment.set_blending_enabled(true); + color_attachment.set_source_rgb_blend_factor(metal::MTLBlendFactor::SourceAlpha); + color_attachment + .set_destination_rgb_blend_factor(metal::MTLBlendFactor::OneMinusSourceAlpha); + color_attachment.set_source_alpha_blend_factor(metal::MTLBlendFactor::One); + color_attachment + .set_destination_alpha_blend_factor(metal::MTLBlendFactor::OneMinusSourceAlpha); + + let pipeline_state = device + .new_render_pipeline_state(&pipeline_descriptor) + .expect("Failed to create pipeline state"); + + self.device = Some(device); + self.pipeline_state = Some(pipeline_state); + } + + #[cfg(target_os = "macos")] + fn create_render_callback(&self) -> MetalRenderCallback { + let pipeline_state = self.pipeline_state.clone().unwrap(); + + Arc::new( + move |encoder: &RenderCommandEncoderRef, + _target: &TextureRef, + bounds: Bounds, + scale_factor: f32| { + // Set the pipeline state + encoder.set_render_pipeline_state(&pipeline_state); + + // Set viewport to match element bounds + let viewport = metal::MTLViewport { + originX: bounds.origin.x.0 as f64 * scale_factor as f64, + originY: bounds.origin.y.0 as f64 * scale_factor as f64, + width: bounds.size.width.0 as f64 * scale_factor as f64, + height: bounds.size.height.0 as f64 * scale_factor as f64, + znear: 0.0, + zfar: 1.0, + }; + encoder.set_viewport(viewport); + + // Set scissor rectangle to clip to bounds + let scissor_rect = metal::MTLScissorRect { + x: (bounds.origin.x.0 * scale_factor) as u64, + y: (bounds.origin.y.0 * scale_factor) as u64, + width: (bounds.size.width.0 * scale_factor) as u64, + height: (bounds.size.height.0 * scale_factor) as u64, + }; + encoder.set_scissor_rect(scissor_rect); + + // Pass viewport size as uniform + #[repr(C)] + struct Uniforms { + viewport_size: [f32; 2], + } + + let uniforms = Uniforms { + viewport_size: [ + bounds.size.width.0 * scale_factor, + bounds.size.height.0 * scale_factor, + ], + }; + + encoder.set_vertex_bytes( + 0, + std::mem::size_of::() as u64, + &uniforms as *const Uniforms as *const _, + ); + + // Draw the quad + encoder.draw_primitives(MTLPrimitiveType::Triangle, 0, 6); + }, + ) + } +} + +impl Render for MetalQuadExample { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + #[cfg(target_os = "macos")] + if self.pipeline_state.is_none() { + self.setup_metal(); + } + + div() + .flex() + .flex_col() + .bg(rgb(0x1e1e1e)) + .size_full() + .p_8() + .gap_6() + .child( + div() + .child("Metal Quad Example") + .text_2xl() + .text_color(rgb(0xffffff)), + ) + .child( + div() + .child("This example demonstrates proper coordinate handling in MetalView") + .text_color(rgb(0xaaaaaa)), + ) + .child( + div() + .flex() + .gap_4() + .child( + div() + .flex_col() + .gap_2() + .flex_1() + .child( + div() + .child("Small MetalView (200x150)") + .text_sm() + .text_color(rgb(0xcccccc)), + ) + .child( + div() + .border_1() + .border_color(rgb(0x444444)) + .rounded_md() + .overflow_hidden() + .child( + #[cfg(target_os = "macos")] + { + let callback = self.create_render_callback(); + metal_view() + .render_with_shared(callback) + .w(px(200.0)) + .h(px(150.0)) + .bg(rgb(0x000000)) + }, + #[cfg(not(target_os = "macos"))] + { + div() + .w(px(200.0)) + .h(px(150.0)) + .bg(rgb(0x222222)) + .flex() + .items_center() + .justify_center() + .child( + div() + .child("Metal (macOS only)") + .text_color(rgb(0x666666)), + ) + }, + ), + ), + ) + .child( + div() + .flex_col() + .gap_2() + .flex_1() + .child( + div() + .child("Large MetalView (400x300)") + .text_sm() + .text_color(rgb(0xcccccc)), + ) + .child( + div() + .border_1() + .border_color(rgb(0x444444)) + .rounded_md() + .overflow_hidden() + .child( + #[cfg(target_os = "macos")] + { + let callback = self.create_render_callback(); + metal_view() + .render_with_shared(callback) + .w(px(400.0)) + .h(px(300.0)) + .bg(rgb(0x000000)) + }, + #[cfg(not(target_os = "macos"))] + { + div() + .w(px(400.0)) + .h(px(300.0)) + .bg(rgb(0x222222)) + .flex() + .items_center() + .justify_center() + .child( + div() + .child("Metal (macOS only)") + .text_color(rgb(0x666666)), + ) + }, + ), + ), + ), + ) + .child( + div().p_4().bg(rgb(0x2a2a2a)).rounded_md().child( + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .child("Key Features:") + .text_base() + .font_weight(FontWeight::SEMIBOLD) + .text_color(rgb(0xffffff)), + ) + .child( + div() + .child("• Proper coordinate transformation from pixels to NDC") + .text_sm() + .text_color(rgb(0xaaaaaa)), + ) + .child( + div() + .child("• Scissor rectangle to clip content to bounds") + .text_sm() + .text_color(rgb(0xaaaaaa)), + ) + .child( + div() + .child("• Viewport size passed as uniform to shader") + .text_sm() + .text_color(rgb(0xaaaaaa)), + ) + .child( + div() + .child("• Gradient fills entire MetalView bounds") + .text_sm() + .text_color(rgb(0xaaaaaa)), + ), + ), + ) + } +} + +fn main() { + Application::new().run(|cx: &mut App| { + let _ = cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(Bounds::centered( + None, + size(px(900.0), px(600.0)), + cx, + ))), + titlebar: Some(TitlebarOptions { + title: Some("Metal Quad Example".into()), + ..Default::default() + }), + ..Default::default() + }, + |_window, cx| cx.new(|_cx| MetalQuadExample::new()), + ); + }); +} diff --git a/crates/gpui/examples/metal_view_simple.rs b/crates/gpui/examples/metal_view_simple.rs new file mode 100644 index 0000000000000000000000000000000000000000..f7dfa4e9c23f3a1e33dddd91cb02f389d68f6867 --- /dev/null +++ b/crates/gpui/examples/metal_view_simple.rs @@ -0,0 +1,198 @@ +use gpui::{prelude::*, *}; + +struct MetalViewSimpleExample { + show_metal_view: bool, +} + +impl MetalViewSimpleExample { + fn new() -> Self { + Self { + show_metal_view: true, + } + } +} + +impl Render for MetalViewSimpleExample { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + div() + .flex() + .flex_col() + .bg(rgb(0x1e1e1e)) + .size_full() + .p_8() + .gap_4() + .child( + div() + .child("MetalView Simple Example") + .text_2xl() + .text_color(rgb(0xffffff)), + ) + .child( + div() + .flex() + .gap_2() + .items_center() + .child( + div() + .id("toggle-button") + .px_3() + .py_1() + .bg(rgb(0x3b82f6)) + .hover(|style| style.bg(rgb(0x2563eb))) + .rounded_md() + .cursor_pointer() + .on_click(cx.listener(|this, _event, _window, cx| { + this.show_metal_view = !this.show_metal_view; + cx.notify(); + })) + .child(div().child("Toggle MetalView").text_color(rgb(0xffffff))), + ) + .child( + div() + .child(format!( + "MetalView is: {}", + if self.show_metal_view { + "visible" + } else { + "hidden" + } + )) + .text_color(rgb(0xaaaaaa)), + ), + ) + .child( + div() + .flex() + .flex_col() + .gap_4() + .p_4() + .bg(rgb(0x2a2a2a)) + .rounded_md() + .child( + div() + .child("Container with MetalView") + .text_lg() + .text_color(rgb(0xffffff)), + ) + .when(self.show_metal_view, |parent| { + parent.child( + div() + .border_2() + .border_color(rgb(0x444444)) + .rounded_md() + .overflow_hidden() + .child( + #[cfg(target_os = "macos")] + { + metal_view() + .w_full() + .h(px(200.0)) + .bg(rgb(0x1a1a1a)) + .render_with( + |_encoder, _target, _bounds, _scale_factor| { + // This callback would contain custom Metal rendering code + // For now, it's just a placeholder + }, + ) + }, + #[cfg(not(target_os = "macos"))] + { + div() + .w_full() + .h(px(200.0)) + .bg(rgb(0x1a1a1a)) + .flex() + .items_center() + .justify_center() + .child( + div() + .child("MetalView (macOS only)") + .text_color(rgb(0x666666)), + ) + }, + ), + ) + }) + .child( + div() + .flex() + .gap_4() + .child( + div().flex_1().p_3().bg(rgb(0x333333)).rounded_md().child( + div() + .child("Regular GPUI content") + .text_sm() + .text_color(rgb(0xcccccc)), + ), + ) + .child( + div().flex_1().p_3().bg(rgb(0x333333)).rounded_md().child( + div() + .child("Can be mixed with MetalView") + .text_sm() + .text_color(rgb(0xcccccc)), + ), + ), + ), + ) + .child( + div().mt_4().p_4().bg(rgb(0x2a2a2a)).rounded_md().child( + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .child("Notes:") + .text_base() + .font_weight(FontWeight::SEMIBOLD) + .text_color(rgb(0xffffff)), + ) + .child( + div() + .child("• MetalView integrates with GPUI's layout system") + .text_sm() + .text_color(rgb(0xaaaaaa)), + ) + .child( + div() + .child("• It can be styled with the same methods as other elements") + .text_sm() + .text_color(rgb(0xaaaaaa)), + ) + .child( + div() + .child("• On macOS, it would render custom Metal content") + .text_sm() + .text_color(rgb(0xaaaaaa)), + ) + .child( + div() + .child("• On other platforms, a fallback can be provided") + .text_sm() + .text_color(rgb(0xaaaaaa)), + ), + ), + ) + } +} + +fn main() { + Application::new().run(|cx: &mut App| { + let _ = cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(Bounds::centered( + None, + size(px(800.0), px(600.0)), + cx, + ))), + titlebar: Some(TitlebarOptions { + title: Some("MetalView Simple Example".into()), + ..Default::default() + }), + ..Default::default() + }, + |_window, cx| cx.new(|_cx| MetalViewSimpleExample::new()), + ); + }); +} diff --git a/crates/gpui/src/elements/metal_view.rs b/crates/gpui/src/elements/metal_view.rs new file mode 100644 index 0000000000000000000000000000000000000000..3ff7308863730b236620cf2dd850c394f5eca53e --- /dev/null +++ b/crates/gpui/src/elements/metal_view.rs @@ -0,0 +1,196 @@ +use crate::{ + App, Bounds, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement, LayoutId, + Pixels, Style, StyleRefinement, Styled, Window, +}; +use refineable::Refineable; +use std::sync::Arc; + +#[cfg(target_os = "macos")] +use metal::{RenderCommandEncoderRef, TextureRef}; + +/// A callback for custom Metal rendering. +/// +/// The callback receives: +/// - command_encoder: The Metal command encoder to issue draw calls +/// - target_texture: The texture to render into +/// - bounds: The bounds of the element in pixels +/// - scale_factor: The window's scale factor +#[cfg(target_os = "macos")] +pub type MetalRenderCallback = + Arc, f32) + Send + Sync + 'static>; + +/// A view that allows custom Metal rendering. +pub struct MetalView { + #[cfg(target_os = "macos")] + render_callback: Option, + style: StyleRefinement, +} + +/// Create a new Metal view element. +pub fn metal_view() -> MetalView { + MetalView { + #[cfg(target_os = "macos")] + render_callback: None, + style: Default::default(), + } +} + +impl MetalView { + /// Set the Metal render callback. + #[cfg(target_os = "macos")] + pub fn render_with(mut self, callback: F) -> Self + where + F: Fn(&RenderCommandEncoderRef, &TextureRef, Bounds, f32) + Send + Sync + 'static, + { + self.render_callback = Some(Arc::new(callback)); + self + } + + /// Set the Metal render callback using a shared callback. + #[cfg(target_os = "macos")] + pub fn render_with_shared(mut self, callback: MetalRenderCallback) -> Self { + self.render_callback = Some(callback); + self + } +} + +impl Element for MetalView { + type RequestLayoutState = (); + type PrepaintState = (); + + fn id(&self) -> Option { + None + } + + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + _global_id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + let mut style = Style::default(); + style.refine(&self.style); + let layout_id = window.request_layout(style, [], cx); + (layout_id, ()) + } + + fn prepaint( + &mut self, + _global_id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + _window: &mut Window, + _cx: &mut App, + ) -> Self::PrepaintState { + } + + fn paint( + &mut self, + _global_id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + _: &mut Self::RequestLayoutState, + _: &mut Self::PrepaintState, + window: &mut Window, + _: &mut App, + ) { + #[cfg(target_os = "macos")] + if let Some(render_callback) = &self.render_callback { + // TODO: This is a placeholder. In a real implementation, we would need to: + // 1. Register this Metal view with the window's rendering system + // 2. Ensure the callback is invoked during the Metal rendering pass + // 3. Handle proper clipping and transformation matrices + // + // For now, we'll store the callback and bounds in the window's custom render queue + window.paint_metal_view(bounds, render_callback.clone()); + } + } +} + +impl IntoElement for MetalView { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +impl Styled for MetalView { + fn style(&mut self) -> &mut StyleRefinement { + &mut self.style + } +} + +/// Extension trait for MetalView to provide platform-agnostic API +pub trait MetalViewExt { + /// Set a placeholder render function for non-macOS platforms + fn render_placeholder(self, callback: F) -> Self + where + F: Fn(Bounds) + Send + Sync + 'static; +} + +impl MetalViewExt for MetalView { + fn render_placeholder(self, _callback: F) -> Self + where + F: Fn(Bounds) + Send + Sync + 'static, + { + // On non-macOS platforms, this could render a placeholder + // or use a different rendering backend + self + } +} + +#[cfg(target_os = "macos")] +/// Helper functions for creating common Metal render callbacks +pub mod helpers { + use super::*; + use metal::*; + + /// Helper to create a simple colored rectangle Metal renderer + pub fn solid_color_renderer(r: f32, g: f32, b: f32, a: f32) -> MetalRenderCallback { + Arc::new(move |encoder, _texture, bounds, _scale_factor| { + // This is a simplified example. In practice, you would: + // 1. Create or reuse a render pipeline state + // 2. Set up vertex data for the bounds + // 3. Issue draw calls + // 4. Handle proper coordinate transformation + + // For now, this is just a placeholder to show the API design + let _ = (encoder, bounds, r, g, b, a); + }) + } + + /// Helper to create a Metal renderer that draws a textured quad + pub fn textured_quad_renderer(texture: Texture) -> MetalRenderCallback { + Arc::new(move |encoder, _target, bounds, _scale_factor| { + // Similar to above, this would set up a textured quad rendering + let _ = (encoder, &texture, bounds); + }) + } +} + +// Example usage: +// ```rust +// use gpui::elements::{metal_view, MetalViewExt}; +// +// #[cfg(target_os = "macos")] +// let view = metal_view() +// .render_with(|encoder, target, bounds, scale_factor| { +// // Custom Metal rendering code here +// // You have full access to Metal command encoder +// }) +// .size_full(); +// +// #[cfg(not(target_os = "macos"))] +// let view = metal_view() +// .render_placeholder(|bounds| { +// // Fallback rendering for non-macOS platforms +// }) +// .size_full(); +// ``` diff --git a/crates/gpui/src/elements/mod.rs b/crates/gpui/src/elements/mod.rs index bfbc08b3f497925e22e61c2f222919ad7c557610..ac992bacae73e2e1f9fd6f285181b48e7fb36783 100644 --- a/crates/gpui/src/elements/mod.rs +++ b/crates/gpui/src/elements/mod.rs @@ -6,6 +6,9 @@ mod div; mod image_cache; mod img; mod list; +/// Metal-based custom rendering for macOS +#[cfg(target_os = "macos")] +pub mod metal_view; mod surface; mod svg; mod text; @@ -19,6 +22,8 @@ pub use div::*; pub use image_cache::*; pub use img::*; pub use list::*; +#[cfg(target_os = "macos")] +pub use metal_view::*; pub use surface::*; pub use svg::*; pub use text::*; diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index 91461a4d2c8f1bbf1504a36429064a038bedec21..f4da7c59e65788e96f745902e92fa19d1cc3fea4 100644 --- a/crates/gpui/src/gpui.rs +++ b/crates/gpui/src/gpui.rs @@ -129,6 +129,8 @@ pub use assets::*; pub use color::*; pub use ctor::ctor; pub use element::*; +#[cfg(target_os = "macos")] +pub use elements::metal_view::MetalRenderCallback; pub use elements::*; pub use executor::*; pub use geometry::*; diff --git a/crates/gpui/src/platform/mac.rs b/crates/gpui/src/platform/mac.rs index cce65e4293001a03bfe5ff7596403d764cd4b6b4..170e6508af59c5b4ec9ed9ddd5bb2ab192227fc0 100644 --- a/crates/gpui/src/platform/mac.rs +++ b/crates/gpui/src/platform/mac.rs @@ -9,6 +9,8 @@ mod screen_capture; #[cfg(not(feature = "macos-blade"))] mod metal_atlas; +// #[cfg(not(feature = "macos-blade"))] +// mod metal_render_pass; #[cfg(not(feature = "macos-blade"))] pub mod metal_renderer; diff --git a/crates/gpui/src/platform/mac/metal_render_pass.rs b/crates/gpui/src/platform/mac/metal_render_pass.rs new file mode 100644 index 0000000000000000000000000000000000000000..bfdddb975fa974cd245fe16aa9063095ceb1eeb4 --- /dev/null +++ b/crates/gpui/src/platform/mac/metal_render_pass.rs @@ -0,0 +1,343 @@ +use crate::{DevicePixels, PaintMetalView, PrimitiveBatch, ScaledPixels, Scene, Size}; +use metal::{ + CommandBufferRef, CommandQueue, Device, MTLLoadAction, MTLStoreAction, RenderCommandEncoderRef, +}; + +/// Represents a single render command in the rendering pipeline +#[derive(Debug)] +pub enum RenderCommand<'a> { + /// Begin a new render pass with the specified configuration + BeginRenderPass { descriptor: RenderPassDescriptor }, + /// Draw a batch of GPUI primitives + DrawPrimitives { + batch: PrimitiveBatch<'a>, + viewport_size: Size, + }, + /// Execute custom Metal rendering + ExecuteMetalCallback { + metal_view: &'a PaintMetalView, + viewport_size: Size, + }, + /// End the current render pass + EndRenderPass, +} + +/// Configuration for a render pass +#[derive(Clone, Debug)] +pub struct RenderPassDescriptor { + pub texture: metal::Texture, + pub load_action: MTLLoadAction, + pub store_action: MTLStoreAction, + pub clear_color: metal::MTLClearColor, + pub viewport: metal::MTLViewport, +} + +/// State that needs to be preserved across render pass breaks +#[derive(Clone, Debug)] +pub struct RenderState { + pub viewport: metal::MTLViewport, + pub blend_mode: Option, + // Add other state that needs to be preserved +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum BlendMode { + Normal, + Multiply, + Screen, + // Add other blend modes as needed +} + +/// Context provided to Metal render callbacks +pub struct MetalRenderContext<'a> { + pub command_buffer: &'a CommandBufferRef, + pub drawable_texture: &'a metal::TextureRef, + pub viewport_size: Size, + pub device: &'a Device, + pub bounds: crate::Bounds, + pub scale_factor: f32, +} + +/// Manages the rendering pipeline with support for render pass breaks +pub struct RenderPassManager { + device: Device, + command_queue: CommandQueue, + current_state: RenderState, +} + +impl RenderPassManager { + pub fn new(device: Device, command_queue: CommandQueue) -> Self { + Self { + device, + command_queue, + current_state: RenderState { + viewport: metal::MTLViewport { + originX: 0.0, + originY: 0.0, + width: 0.0, + height: 0.0, + znear: 0.0, + zfar: 1.0, + }, + blend_mode: None, + }, + } + } + + /// Convert a scene into a list of render commands + pub fn build_render_commands<'a>( + &self, + scene: &'a Scene, + drawable_texture: &metal::TextureRef, + viewport_size: Size, + is_opaque: bool, + ) -> Vec> { + let mut commands = Vec::new(); + + // Initial render pass configuration + let alpha = if is_opaque { 1.0 } else { 0.0 }; + let descriptor = RenderPassDescriptor { + texture: drawable_texture.to_owned(), + load_action: MTLLoadAction::Clear, + store_action: MTLStoreAction::Store, + clear_color: metal::MTLClearColor::new(0.0, 0.0, 0.0, alpha), + viewport: metal::MTLViewport { + originX: 0.0, + originY: 0.0, + width: i32::from(viewport_size.width) as f64, + height: i32::from(viewport_size.height) as f64, + znear: 0.0, + zfar: 1.0, + }, + }; + + commands.push(RenderCommand::BeginRenderPass { descriptor }); + + // Process batches, inserting render pass breaks for MetalViews + let mut in_render_pass = true; + + for batch in scene.batches() { + match batch { + #[cfg(target_os = "macos")] + PrimitiveBatch::MetalViews(metal_views) => { + // End current render pass + if in_render_pass { + commands.push(RenderCommand::EndRenderPass); + in_render_pass = false; + } + + // Add commands for each MetalView + for metal_view in metal_views { + commands.push(RenderCommand::ExecuteMetalCallback { + metal_view, + viewport_size, + }); + } + } + _ => { + // Ensure we're in a render pass + if !in_render_pass { + let descriptor = RenderPassDescriptor { + texture: drawable_texture.to_owned(), + load_action: MTLLoadAction::Load, // Load existing content + store_action: MTLStoreAction::Store, + clear_color: metal::MTLClearColor::new(0.0, 0.0, 0.0, 0.0), + viewport: self.current_state.viewport, + }; + commands.push(RenderCommand::BeginRenderPass { descriptor }); + in_render_pass = true; + } + + // Add primitive drawing command + commands.push(RenderCommand::DrawPrimitives { + batch, + viewport_size, + }); + } + } + } + + // Ensure we end the final render pass + if in_render_pass { + commands.push(RenderCommand::EndRenderPass); + } + + commands + } + + /// Execute a list of render commands + pub fn execute_commands( + &mut self, + commands: &[RenderCommand], + command_buffer: &CommandBufferRef, + drawable_texture: &metal::TextureRef, + mut draw_primitives: F, + ) -> Result<(), anyhow::Error> + where + F: FnMut( + PrimitiveBatch, + &RenderCommandEncoderRef, + Size, + ) -> Result<(), anyhow::Error>, + { + let mut current_encoder: Option = None; + + for command in commands { + match command { + RenderCommand::BeginRenderPass { descriptor } => { + // End any existing encoder + if let Some(encoder) = current_encoder.take() { + encoder.end_encoding(); + } + + // Create new render pass + let render_pass_descriptor = metal::RenderPassDescriptor::new(); + let color_attachment = render_pass_descriptor + .color_attachments() + .object_at(0) + .unwrap(); + + color_attachment.set_texture(Some(&descriptor.texture)); + color_attachment.set_load_action(descriptor.load_action); + color_attachment.set_store_action(descriptor.store_action); + color_attachment.set_clear_color(descriptor.clear_color); + + let encoder = + command_buffer.new_render_command_encoder(&render_pass_descriptor); + encoder.set_viewport(descriptor.viewport); + self.current_state.viewport = descriptor.viewport; + + current_encoder = Some(encoder); + } + + RenderCommand::DrawPrimitives { + batch, + viewport_size, + } => { + if let Some(ref encoder) = current_encoder { + draw_primitives(*batch, encoder, *viewport_size)?; + } + } + + RenderCommand::ExecuteMetalCallback { + metal_view, + viewport_size, + } => { + // End current encoder if any + if let Some(encoder) = current_encoder.take() { + encoder.end_encoding(); + } + + // Create context for the callback + let context = MetalRenderContext { + command_buffer, + drawable_texture, + viewport_size: *viewport_size, + device: &self.device, + bounds: metal_view.bounds.clone(), + scale_factor: 2.0, // TODO: Get actual scale factor + }; + + // Create a new render command encoder for the callback + let render_pass_descriptor = metal::RenderPassDescriptor::new(); + let color_attachment = render_pass_descriptor + .color_attachments() + .object_at(0) + .unwrap(); + + color_attachment.set_texture(Some(drawable_texture)); + color_attachment.set_load_action(MTLLoadAction::Load); + color_attachment.set_store_action(MTLStoreAction::Store); + + let encoder = + command_buffer.new_render_command_encoder(&render_pass_descriptor); + + // Invoke the callback + (metal_view.render_callback)( + &encoder, + drawable_texture, + context.bounds.into(), + context.scale_factor, + ); + + encoder.end_encoding(); + } + + RenderCommand::EndRenderPass => { + if let Some(encoder) = current_encoder.take() { + encoder.end_encoding(); + } + } + } + } + + // Ensure any remaining encoder is ended + if let Some(encoder) = current_encoder { + encoder.end_encoding(); + } + + Ok(()) + } + + /// Save the current render state + pub fn save_state(&self) -> RenderState { + self.current_state.clone() + } + + /// Restore a previously saved render state + pub fn restore_state(&mut self, state: RenderState) { + self.current_state = state; + } +} + +/// Builder for constructing render command lists +pub struct RenderCommandBuilder<'a> { + commands: Vec>, +} + +impl<'a> RenderCommandBuilder<'a> { + pub fn new() -> Self { + Self { + commands: Vec::new(), + } + } + + pub fn begin_render_pass(mut self, descriptor: RenderPassDescriptor) -> Self { + self.commands + .push(RenderCommand::BeginRenderPass { descriptor }); + self + } + + pub fn draw_primitives( + mut self, + batch: PrimitiveBatch<'a>, + viewport_size: Size, + ) -> Self { + self.commands.push(RenderCommand::DrawPrimitives { + batch, + viewport_size, + }); + self + } + + pub fn execute_metal_callback( + mut self, + metal_view: &'a PaintMetalView, + viewport_size: Size, + ) -> Self { + self.commands.push(RenderCommand::ExecuteMetalCallback { + metal_view, + viewport_size, + }); + self + } + + pub fn end_render_pass(mut self) -> Self { + self.commands.push(RenderCommand::EndRenderPass); + self + } + + pub fn build(self) -> Vec> { + self.commands + } +} diff --git a/crates/gpui/src/platform/mac/metal_renderer.rs b/crates/gpui/src/platform/mac/metal_renderer.rs index 3cdc2dd2cf42ea7c2a92152893679aa930466869..a89c7347745906d5400f83e188d0b56f34b811e1 100644 --- a/crates/gpui/src/platform/mac/metal_renderer.rs +++ b/crates/gpui/src/platform/mac/metal_renderer.rs @@ -2,7 +2,7 @@ use super::metal_atlas::MetalAtlas; use crate::{ AtlasTextureId, AtlasTextureKind, AtlasTile, Background, Bounds, ContentMask, DevicePixels, MonochromeSprite, PaintSurface, Path, PathId, PathVertex, PolychromeSprite, PrimitiveBatch, - Quad, ScaledPixels, Scene, Shadow, Size, Surface, Underline, point, size, + Quad, ScaledPixels, Scene, Shadow, Size, Surface, Underline, point, px, size, }; use anyhow::{Context as _, Result}; use block::ConcreteBlock; @@ -18,7 +18,7 @@ use core_video::{ pixel_buffer::kCVPixelFormatType_420YpCbCr8BiPlanarFullRange, }; use foreign_types::{ForeignType, ForeignTypeRef}; -use metal::{CAMetalLayer, CommandQueue, MTLPixelFormat, MTLResourceOptions, NSRange}; +use metal::{CAMetalLayer, MTLPixelFormat, MTLResourceOptions, NSRange}; use objc::{self, msg_send, sel, sel_impl}; use parking_lot::Mutex; use smallvec::SmallVec; @@ -97,7 +97,7 @@ pub(crate) struct MetalRenderer { device: metal::Device, layer: metal::MetalLayer, presents_with_transaction: bool, - command_queue: CommandQueue, + command_queue: metal::CommandQueue, paths_rasterization_pipeline_state: metal::RenderPipelineState, path_sprites_pipeline_state: metal::RenderPipelineState, shadows_pipeline_state: metal::RenderPipelineState, @@ -385,6 +385,7 @@ impl MetalRenderer { ) .with_context(|| format!("rasterizing {} paths", scene.paths().len()))?; + // Create initial render pass let render_pass_descriptor = metal::RenderPassDescriptor::new(); let color_attachment = render_pass_descriptor .color_attachments() @@ -396,7 +397,7 @@ impl MetalRenderer { color_attachment.set_store_action(metal::MTLStoreAction::Store); let alpha = if self.layer.is_opaque() { 1. } else { 0. }; color_attachment.set_clear_color(metal::MTLClearColor::new(0., 0., 0., alpha)); - let command_encoder = command_buffer.new_render_command_encoder(render_pass_descriptor); + let mut command_encoder = command_buffer.new_render_command_encoder(render_pass_descriptor); command_encoder.set_viewport(metal::MTLViewport { originX: 0.0, @@ -407,21 +408,53 @@ impl MetalRenderer { zfar: 1.0, }); + let mut needs_new_encoder = false; + + // Helper to create a continuation render encoder + let create_continuation_encoder = || { + let render_pass_descriptor = metal::RenderPassDescriptor::new(); + let color_attachment = render_pass_descriptor + .color_attachments() + .object_at(0) + .unwrap(); + + color_attachment.set_texture(Some(drawable.texture())); + color_attachment.set_load_action(metal::MTLLoadAction::Load); + color_attachment.set_store_action(metal::MTLStoreAction::Store); + + let encoder = command_buffer.new_render_command_encoder(render_pass_descriptor); + encoder.set_viewport(metal::MTLViewport { + originX: 0.0, + originY: 0.0, + width: i32::from(viewport_size.width) as f64, + height: i32::from(viewport_size.height) as f64, + znear: 0.0, + zfar: 1.0, + }); + encoder + }; + for batch in scene.batches() { + // Create a new encoder if needed + if needs_new_encoder { + command_encoder = create_continuation_encoder(); + needs_new_encoder = false; + } + let ok = match batch { PrimitiveBatch::Shadows(shadows) => self.draw_shadows( shadows, instance_buffer, &mut instance_offset, viewport_size, - command_encoder, + &command_encoder, ), PrimitiveBatch::Quads(quads) => self.draw_quads( quads, instance_buffer, &mut instance_offset, viewport_size, - command_encoder, + &command_encoder, ), PrimitiveBatch::Paths(paths) => self.draw_paths( paths, @@ -429,14 +462,14 @@ impl MetalRenderer { instance_buffer, &mut instance_offset, viewport_size, - command_encoder, + &command_encoder, ), PrimitiveBatch::Underlines(underlines) => self.draw_underlines( underlines, instance_buffer, &mut instance_offset, viewport_size, - command_encoder, + &command_encoder, ), PrimitiveBatch::MonochromeSprites { texture_id, @@ -447,7 +480,7 @@ impl MetalRenderer { instance_buffer, &mut instance_offset, viewport_size, - command_encoder, + &command_encoder, ), PrimitiveBatch::PolychromeSprites { texture_id, @@ -458,15 +491,72 @@ impl MetalRenderer { instance_buffer, &mut instance_offset, viewport_size, - command_encoder, + &command_encoder, ), PrimitiveBatch::Surfaces(surfaces) => self.draw_surfaces( surfaces, instance_buffer, &mut instance_offset, viewport_size, - command_encoder, + &command_encoder, ), + #[cfg(target_os = "macos")] + PrimitiveBatch::MetalViews(metal_views) => { + // End current render pass + command_encoder.end_encoding(); + + // Process each MetalView + for metal_view in metal_views { + // Create a render encoder for the callback + let render_pass_descriptor = metal::RenderPassDescriptor::new(); + let color_attachment = render_pass_descriptor + .color_attachments() + .object_at(0) + .unwrap(); + + color_attachment.set_texture(Some(drawable.texture())); + color_attachment.set_load_action(metal::MTLLoadAction::Load); + color_attachment.set_store_action(metal::MTLStoreAction::Store); + + let callback_encoder = + command_buffer.new_render_command_encoder(render_pass_descriptor); + callback_encoder.set_viewport(metal::MTLViewport { + originX: 0.0, + originY: 0.0, + width: i32::from(viewport_size.width) as f64, + height: i32::from(viewport_size.height) as f64, + znear: 0.0, + zfar: 1.0, + }); + + // Invoke the Metal rendering callback + let scale_factor = self.layer.contents_scale() as f32; + // Convert bounds from ScaledPixels to Pixels + let bounds = Bounds { + origin: point( + px(metal_view.bounds.origin.x.0 / scale_factor), + px(metal_view.bounds.origin.y.0 / scale_factor), + ), + size: size( + px(metal_view.bounds.size.width.0 / scale_factor), + px(metal_view.bounds.size.height.0 / scale_factor), + ), + }; + + (metal_view.render_callback)( + &callback_encoder, + drawable.texture(), + bounds, + scale_factor, + ); + + callback_encoder.end_encoding(); + } + + // Mark that we'll need a new encoder for subsequent primitives + needs_new_encoder = true; + true + } }; if !ok { @@ -484,7 +574,10 @@ impl MetalRenderer { } } - command_encoder.end_encoding(); + // End the encoder if we haven't already + if !needs_new_encoder { + command_encoder.end_encoding(); + } instance_buffer.metal_buffer.did_modify_range(NSRange { location: 0, @@ -1134,6 +1227,9 @@ impl MetalRenderer { } true } + + // Note: draw_metal_views is no longer needed as we handle MetalViews + // directly in draw_primitives with proper render pass management } fn build_pipeline_state( diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index 4eaef64afa1d0d888d93dceca07569136edb0d8e..83b79a53293f27fcf76abf3ec8d743777f522c22 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -27,6 +27,8 @@ pub(crate) struct Scene { pub(crate) monochrome_sprites: Vec, pub(crate) polychrome_sprites: Vec, pub(crate) surfaces: Vec, + #[cfg(target_os = "macos")] + pub(crate) metal_views: Vec, } impl Scene { @@ -115,6 +117,11 @@ impl Scene { surface.order = order; self.surfaces.push(surface.clone()); } + #[cfg(target_os = "macos")] + Primitive::MetalView(metal_view) => { + metal_view.order = order; + self.metal_views.push(metal_view.clone()); + } } self.paint_operations .push(PaintOperation::Primitive(primitive)); @@ -140,6 +147,8 @@ impl Scene { self.polychrome_sprites .sort_by_key(|sprite| (sprite.order, sprite.tile.tile_id)); self.surfaces.sort_by_key(|surface| surface.order); + #[cfg(target_os = "macos")] + self.metal_views.sort_by_key(|metal_view| metal_view.order); } #[cfg_attr( @@ -172,6 +181,12 @@ impl Scene { surfaces: &self.surfaces, surfaces_start: 0, surfaces_iter: self.surfaces.iter().peekable(), + #[cfg(target_os = "macos")] + metal_views: &self.metal_views, + #[cfg(target_os = "macos")] + metal_views_start: 0, + #[cfg(target_os = "macos")] + metal_views_iter: self.metal_views.iter().peekable(), } } } @@ -193,6 +208,8 @@ pub(crate) enum PrimitiveKind { MonochromeSprite, PolychromeSprite, Surface, + #[cfg(target_os = "macos")] + MetalView, } pub(crate) enum PaintOperation { @@ -210,6 +227,8 @@ pub(crate) enum Primitive { MonochromeSprite(MonochromeSprite), PolychromeSprite(PolychromeSprite), Surface(PaintSurface), + #[cfg(target_os = "macos")] + MetalView(PaintMetalView), } impl Primitive { @@ -222,6 +241,8 @@ impl Primitive { Primitive::MonochromeSprite(sprite) => &sprite.bounds, Primitive::PolychromeSprite(sprite) => &sprite.bounds, Primitive::Surface(surface) => &surface.bounds, + #[cfg(target_os = "macos")] + Primitive::MetalView(metal_view) => &metal_view.bounds, } } @@ -234,6 +255,8 @@ impl Primitive { Primitive::MonochromeSprite(sprite) => &sprite.content_mask, Primitive::PolychromeSprite(sprite) => &sprite.content_mask, Primitive::Surface(surface) => &surface.content_mask, + #[cfg(target_os = "macos")] + Primitive::MetalView(metal_view) => &metal_view.content_mask, } } } @@ -267,13 +290,19 @@ struct BatchIterator<'a> { surfaces: &'a [PaintSurface], surfaces_start: usize, surfaces_iter: Peekable>, + #[cfg(target_os = "macos")] + metal_views: &'a [PaintMetalView], + #[cfg(target_os = "macos")] + metal_views_start: usize, + #[cfg(target_os = "macos")] + metal_views_iter: Peekable>, } impl<'a> Iterator for BatchIterator<'a> { type Item = PrimitiveBatch<'a>; fn next(&mut self) -> Option { - let mut orders_and_kinds = [ + let mut orders_and_kinds = vec![ ( self.shadows_iter.peek().map(|s| s.order), PrimitiveKind::Shadow, @@ -297,6 +326,12 @@ impl<'a> Iterator for BatchIterator<'a> { PrimitiveKind::Surface, ), ]; + + #[cfg(target_os = "macos")] + orders_and_kinds.push(( + self.metal_views_iter.peek().map(|m| m.order), + PrimitiveKind::MetalView, + )); orders_and_kinds.sort_by_key(|(order, kind)| (order.unwrap_or(u32::MAX), *kind)); let first = orders_and_kinds[0]; @@ -426,6 +461,23 @@ impl<'a> Iterator for BatchIterator<'a> { &self.surfaces[surfaces_start..surfaces_end], )) } + #[cfg(target_os = "macos")] + PrimitiveKind::MetalView => { + let metal_views_start = self.metal_views_start; + let mut metal_views_end = metal_views_start + 1; + self.metal_views_iter.next(); + while self + .metal_views_iter + .next_if(|metal_view| (metal_view.order, batch_kind) < max_order_and_kind) + .is_some() + { + metal_views_end += 1; + } + self.metal_views_start = metal_views_end; + Some(PrimitiveBatch::MetalViews( + &self.metal_views[metal_views_start..metal_views_end], + )) + } } } } @@ -452,6 +504,8 @@ pub(crate) enum PrimitiveBatch<'a> { sprites: &'a [PolychromeSprite], }, Surfaces(&'a [PaintSurface]), + #[cfg(target_os = "macos")] + MetalViews(&'a [PaintMetalView]), } #[derive(Default, Debug, Clone)] @@ -668,12 +722,38 @@ pub(crate) struct PaintSurface { pub image_buffer: core_video::pixel_buffer::CVPixelBuffer, } +#[cfg(target_os = "macos")] +#[derive(Clone)] +pub(crate) struct PaintMetalView { + pub order: DrawOrder, + pub bounds: Bounds, + pub content_mask: ContentMask, + pub render_callback: crate::MetalRenderCallback, +} + +impl Debug for PaintMetalView { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PaintMetalView") + .field("order", &self.order) + .field("bounds", &self.bounds) + .field("content_mask", &self.content_mask) + .finish() + } +} + impl From for Primitive { fn from(surface: PaintSurface) -> Self { Primitive::Surface(surface) } } +#[cfg(target_os = "macos")] +impl From for Primitive { + fn from(metal_view: PaintMetalView) -> Self { + Primitive::MetalView(metal_view) + } +} + #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] pub(crate) struct PathId(pub(crate) usize); diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index be3b753d6ad487eec203a7ea321ea52818a8cad2..8d697ecd386f1dbc096063f98b46f6e761275821 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -2946,6 +2946,30 @@ impl Window { }); } + /// Paint a custom Metal view. + /// + /// This method should only be called as part of the paint phase of element drawing. + #[cfg(target_os = "macos")] + pub fn paint_metal_view( + &mut self, + bounds: Bounds, + render_callback: crate::MetalRenderCallback, + ) { + use crate::PaintMetalView; + + self.invalidator.debug_assert_paint(); + + let scale_factor = self.scale_factor(); + let bounds = bounds.scale(scale_factor); + let content_mask = self.content_mask().scale(scale_factor); + self.next_frame.scene.insert_primitive(PaintMetalView { + order: 0, + bounds, + content_mask, + render_callback, + }); + } + /// Removes an image from the sprite atlas. pub fn drop_image(&mut self, data: Arc) -> Result<()> { for frame_index in 0..data.frame_count() {