1use gpui::{prelude::*, *};
2use std::sync::Arc;
3use std::time::Instant;
4
5#[cfg(target_os = "macos")]
6use metal::{Device, MTLPrimitiveType, RenderCommandEncoderRef, RenderPipelineState, TextureRef};
7
8struct MetalViewExample {
9 start_time: Instant,
10 #[cfg(target_os = "macos")]
11 pipeline_state: Option<RenderPipelineState>,
12 #[cfg(target_os = "macos")]
13 device: Option<Device>,
14}
15
16impl MetalViewExample {
17 fn new() -> Self {
18 Self {
19 #[cfg(target_os = "macos")]
20 pipeline_state: None,
21 #[cfg(target_os = "macos")]
22 device: None,
23 start_time: Instant::now(),
24 }
25 }
26
27 #[cfg(target_os = "macos")]
28 fn setup_metal(&mut self) {
29 let device = Device::system_default().expect("no Metal device");
30
31 // Simplified shader for debugging
32 let shader_source = r#"
33 #include <metal_stdlib>
34 using namespace metal;
35
36 struct Uniforms {
37 float time;
38 };
39
40 struct VertexOut {
41 float4 position [[position]];
42 float4 color;
43 };
44
45 vertex VertexOut vertex_main(
46 uint vid [[vertex_id]],
47 constant Uniforms& uniforms [[buffer(0)]]
48 ) {
49 VertexOut out;
50
51 // Define triangle vertices in normalized device coordinates
52 float2 positions[3] = {
53 float2( 0.0, 0.5), // Top
54 float2(-0.5, -0.5), // Bottom left
55 float2( 0.5, -0.5) // Bottom right
56 };
57
58 float3 colors[3] = {
59 float3(1.0, 0.0, 0.0), // Red
60 float3(0.0, 1.0, 0.0), // Green
61 float3(0.0, 0.0, 1.0) // Blue
62 };
63
64 // Apply rotation
65 float2 pos = positions[vid];
66 float c = cos(uniforms.time);
67 float s = sin(uniforms.time);
68 float2 rotated = float2(
69 pos.x * c - pos.y * s,
70 pos.x * s + pos.y * c
71 );
72
73 out.position = float4(rotated, 0.0, 1.0);
74 out.color = float4(colors[vid], 1.0);
75 return out;
76 }
77
78 fragment float4 fragment_main(VertexOut in [[stage_in]]) {
79 return in.color;
80 }
81 "#;
82
83 let library = device
84 .new_library_with_source(shader_source, &metal::CompileOptions::new())
85 .expect("Failed to create shader library");
86
87 let vertex_function = library.get_function("vertex_main", None).unwrap();
88 let fragment_function = library.get_function("fragment_main", None).unwrap();
89
90 // Create pipeline state - no vertex descriptor needed for vertex_id based rendering
91 let pipeline_descriptor = metal::RenderPipelineDescriptor::new();
92 pipeline_descriptor.set_vertex_function(Some(&vertex_function));
93 pipeline_descriptor.set_fragment_function(Some(&fragment_function));
94
95 let color_attachment = pipeline_descriptor
96 .color_attachments()
97 .object_at(0)
98 .unwrap();
99 color_attachment.set_pixel_format(metal::MTLPixelFormat::BGRA8Unorm);
100
101 // Note: Depth testing is not enabled for now as it requires proper depth buffer setup
102 // in the GPUI rendering pipeline
103
104 // Enable blending
105 color_attachment.set_blending_enabled(true);
106 color_attachment.set_source_rgb_blend_factor(metal::MTLBlendFactor::SourceAlpha);
107 color_attachment
108 .set_destination_rgb_blend_factor(metal::MTLBlendFactor::OneMinusSourceAlpha);
109 color_attachment.set_source_alpha_blend_factor(metal::MTLBlendFactor::One);
110 color_attachment
111 .set_destination_alpha_blend_factor(metal::MTLBlendFactor::OneMinusSourceAlpha);
112
113 let pipeline_state = device
114 .new_render_pipeline_state(&pipeline_descriptor)
115 .expect("Failed to create pipeline state");
116
117 self.device = Some(device);
118 self.pipeline_state = Some(pipeline_state);
119 }
120
121 #[cfg(target_os = "macos")]
122 fn create_render_callback(&self, time_delta: f32) -> MetalRenderCallback {
123 let pipeline_state = self.pipeline_state.clone().unwrap();
124
125 Arc::new(
126 move |encoder: &RenderCommandEncoderRef,
127 _target: &TextureRef,
128 bounds: Bounds<Pixels>,
129 scale_factor: f32| {
130 // Set the pipeline state
131 encoder.set_render_pipeline_state(&pipeline_state);
132
133 // Set viewport to match element bounds
134 let viewport = metal::MTLViewport {
135 originX: bounds.origin.x.0 as f64 * scale_factor as f64,
136 originY: bounds.origin.y.0 as f64 * scale_factor as f64,
137 width: bounds.size.width.0 as f64 * scale_factor as f64,
138 height: bounds.size.height.0 as f64 * scale_factor as f64,
139 znear: 0.0,
140 zfar: 1.0,
141 };
142 encoder.set_viewport(viewport);
143
144 // Set scissor rectangle to clip to bounds
145 let scissor_rect = metal::MTLScissorRect {
146 x: (bounds.origin.x.0 * scale_factor) as u64,
147 y: (bounds.origin.y.0 * scale_factor) as u64,
148 width: (bounds.size.width.0 * scale_factor) as u64,
149 height: (bounds.size.height.0 * scale_factor) as u64,
150 };
151 encoder.set_scissor_rect(scissor_rect);
152
153 // Pass time as uniform
154 let time = time_delta * 2.0; // Scale for reasonable rotation speed
155 #[repr(C)]
156 struct Uniforms {
157 time: f32,
158 }
159 let uniforms = Uniforms { time };
160 encoder.set_vertex_bytes(
161 0,
162 std::mem::size_of::<Uniforms>() as u64,
163 &uniforms as *const Uniforms as *const _,
164 );
165
166 // Draw triangle using vertex_id - no vertex buffer needed
167 encoder.draw_primitives(MTLPrimitiveType::Triangle, 0, 3);
168 },
169 )
170 }
171}
172
173impl Render for MetalViewExample {
174 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
175 // Initialize Metal on first render if on macOS
176 #[cfg(target_os = "macos")]
177 if self.pipeline_state.is_none() {
178 self.setup_metal();
179 }
180
181 // Request animation frame
182 window.request_animation_frame();
183
184 div()
185 .flex()
186 .flex_col()
187 .bg(rgb(0x1e1e1e))
188 .size_full()
189 .p_8()
190 .gap_6()
191 .child(
192 div()
193 .child("Metal View Element")
194 .text_2xl()
195 .text_color(rgb(0xffffff)),
196 )
197 .child(
198 div()
199 .child("While GPUI normally handles all Metal rendering for you, the metal_view() element gives you direct access to write custom Metal shaders and GPU drawing commands")
200 .text_color(rgb(0xaaaaaa)),
201 )
202 .child(
203 div()
204 .child("This example shows a rotating 3D cube - the 'Hello World' of 3D graphics programming")
205 .text_sm()
206 .text_color(rgb(0x888888)),
207 )
208 .child(div().overflow_hidden().child(
209 #[cfg(target_os = "macos")]
210 {
211 let elapsed = self.start_time.elapsed().as_secs_f32();
212 let callback = self.create_render_callback(elapsed);
213 metal_view()
214 .render_with_shared(callback)
215 .w(px(600.0))
216 .h(px(400.0))
217 .bg(rgb(0x000000))
218 },
219 #[cfg(not(target_os = "macos"))]
220 {
221 div()
222 .w(px(600.0))
223 .h(px(400.0))
224 .bg(rgb(0x222222))
225 .flex()
226 .items_center()
227 .justify_center()
228 .child(div().child("Metal (macOS only)").text_color(rgb(0x666666)))
229 },
230 ))
231 }
232}
233
234fn main() {
235 Application::new().run(|cx: &mut App| {
236 let _ = cx.open_window(
237 WindowOptions {
238 window_bounds: Some(WindowBounds::Windowed(Bounds::centered(
239 None,
240 size(px(900.0), px(600.0)),
241 cx,
242 ))),
243 titlebar: Some(TitlebarOptions {
244 title: Some("Metal View Element".into()),
245 ..Default::default()
246 }),
247 ..Default::default()
248 },
249 |_window, cx| cx.new(|_cx| MetalViewExample::new()),
250 );
251
252 cx.activate(false);
253 });
254}