Skip to main content

cvkg_render_native/
lib.rs

1//! # CVKG Agentic Development Guidelines (v1.2)
2//!
3//! All AI agents contributing to this crate MUST follow ALL seven rules:
4//!
5//! ── Karpathy Guidelines (1–4) ────────────────────────────────────────────
6//! 1. THINK FIRST     — State assumptions. Surface ambiguity. Push back on complexity.
7//! 2. STAY SIMPLE     — Minimum code. No speculative features. No unasked-for abstractions.
8//! 3. BE SURGICAL     — Touch only what's required. Own your orphans. Don't improve neighbors.
9//! 4. VERIFY GOALS    — Turn tasks into checkable criteria. Loop until they pass. Never commit broken.
10//!
11//! ── CVKG Extended Protocols (5–7) ────────────────────────────────────────
12//! 5. TRIPLE-PASS     — Read the target, its surrounding context, and its full call graph
13//                      at least THREE TIMES before making any edit or revision.
14//! 6. COMMENT ALL     — Every major pub fn, unsafe block, and non-trivial algorithm in
15//                      every .rs/.ts/.h/.wgsl file MUST have a descriptive doc comment.
16//                      Comments describe WHY and WHAT CONTRACT, not HOW mechanically.
17//! 7. MONITOR LOOPS   — Check every tool call / command for progress every 30 seconds.
18//                      After 3 consecutive identical failures, stop, write BLOCKED.md,
19//                      and move to unblocked work. Never silently accept a broken state.
20//!
21//! Sources:
22//   Karpathy: https://github.com/multica-ai/andrej-karpathy-skills
23//   CVKG Extended: Section 2 of the CVKG Design Specification
24
25//! Platform-native widget delegation using winit and AccessKit
26//!
27//! This crate provides platform-specific rendering backends for native desktop targets
28//  using winit for window/event handling and AccessKit for accessibility tree integration.
29
30use cvkg_core::FrameRenderer;
31use std::sync::Arc;
32use winit::{
33    application::ApplicationHandler,
34    event::WindowEvent,
35    event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
36    window::{Window, WindowId},
37};
38
39/// Native renderer backend implementing the Renderer trait.
40/// It wraps a SurtrRenderer for high-performance GPU drawing.
41pub struct NativeRenderer {
42    window: Arc<Window>,
43    gpu: Option<cvkg_render_gpu::SurtrRenderer>,
44}
45
46impl NativeRenderer {
47    /// Create a new NativeRenderer (internal use by App)
48    fn new(window: Arc<Window>) -> Self {
49        Self {
50            window,
51            gpu: None,
52        }
53    }
54
55    /// Start the CVKG native application with the given view.
56    /// This is the main entry point for desktop applications.
57    pub fn run<V: cvkg_core::View + 'static>(view: V) {
58        let event_loop = EventLoop::new().expect("Failed to create event loop");
59        event_loop.set_control_flow(ControlFlow::Wait);
60
61        let mut app = App {
62            view,
63            renderer: None,
64            accesskit_adapter: None,
65            vdom: Some(cvkg_vdom::VDom::new()),
66            asset_manager: std::sync::Arc::new(NativeAssetManager::new()),
67            cursor_pos: [0.0, 0.0],
68        };
69
70        event_loop.run_app(&mut app).expect("Event loop error");
71    }
72}
73
74struct App<V: cvkg_core::View> {
75    view: V,
76    renderer: Option<NativeRenderer>,
77    accesskit_adapter: Option<accesskit_winit::Adapter>,
78    vdom: Option<cvkg_vdom::VDom>,
79    asset_manager: std::sync::Arc<NativeAssetManager>,
80    cursor_pos: [f32; 2],
81}
82
83impl<V: cvkg_core::View + 'static> ApplicationHandler for App<V> {
84    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
85        let window_attrs = Window::default_attributes()
86            .with_title("CVKG Forge")
87            .with_inner_size(winit::dpi::LogicalSize::new(1280.0, 720.0));
88        
89        let window = Arc::new(event_loop.create_window(window_attrs).expect("Failed to create window"));
90        
91        // Initialize AccessKit adapter
92        let adapter = accesskit_winit::Adapter::with_direct_handlers(
93            event_loop,
94            &window,
95            ShieldWall,
96            ShieldWall,
97            ShieldWall,
98        );
99        self.accesskit_adapter = Some(adapter);
100
101        let mut renderer = NativeRenderer::new(window.clone());
102        
103        // Use a Runtime to block on the async forge process
104        let rt = tokio::runtime::Runtime::new().unwrap();
105        renderer.gpu = Some(rt.block_on(cvkg_render_gpu::SurtrRenderer::forge(window.clone())));
106        
107        self.renderer = Some(renderer);
108
109        // Register AssetManager in the environment
110        cvkg_core::env::insert::<cvkg_core::AssetKey>(self.asset_manager.clone());
111    }
112
113    fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
114        let renderer = if let Some(r) = &mut self.renderer { r } else { return };
115
116        match event {
117            WindowEvent::CloseRequested => event_loop.exit(),
118            WindowEvent::Resized(physical_size) => {
119                if let Some(gpu) = &mut renderer.gpu {
120                    gpu.resize(physical_size.width, physical_size.height);
121                }
122                renderer.window.request_redraw();
123            }
124            WindowEvent::RedrawRequested => {
125                if let Some(gpu) = &mut renderer.gpu {
126                    let encoder = gpu.begin_frame();
127                    
128                    let size = renderer.window.inner_size();
129                    let rect = cvkg_core::Rect {
130                        x: 0.0,
131                        y: 0.0,
132                        width: size.width as f32,
133                        height: size.height as f32,
134                    };
135                    
136                    // Render the view tree
137                    self.view.render(gpu, rect);
138                    
139                    // Update VDOM and Accessibility Tree
140                    let new_vdom = cvkg_vdom::VDom::build(&self.view, rect);
141                    if let (Some(prev_vdom), Some(adapter)) = (&mut self.vdom, &mut self.accesskit_adapter) {
142                        let patches = prev_vdom.diff(&new_vdom);
143                        
144                        // Generate AccessKit updates from patches
145                        let mut nodes = Vec::new();
146                        for patch in patches {
147                            match patch {
148                                cvkg_vdom::VDomPatch::Create(node) | cvkg_vdom::VDomPatch::Replace { node, .. } => {
149                                    nodes.push((accesskit::NodeId(node.id.0 as u64), node.to_accesskit_node()));
150                                }
151                                _ => {}
152                            }
153                        }
154                        
155                        if !nodes.is_empty() {
156                            adapter.update_if_active(|| accesskit::TreeUpdate {
157                                nodes,
158                                tree: None,
159                                focus: accesskit::NodeId(1),
160                            });
161                        }
162                    }
163                    self.vdom = Some(new_vdom);
164                    
165                    gpu.end_frame(encoder);
166                }
167            }
168            WindowEvent::CursorMoved { position, .. } => {
169                let scale = renderer.window.scale_factor() as f32;
170                self.cursor_pos = [position.x as f32 / scale, position.y as f32 / scale];
171                if let Some(vdom) = &self.vdom {
172                    vdom.dispatch_event(cvkg_core::Event::PointerMove { x: self.cursor_pos[0], y: self.cursor_pos[1] });
173                }
174            }
175            WindowEvent::MouseInput { state, .. } => {
176                if let Some(vdom) = &self.vdom {
177                    let event = match state {
178                        winit::event::ElementState::Pressed => cvkg_core::Event::PointerDown { x: self.cursor_pos[0], y: self.cursor_pos[1] },
179                        winit::event::ElementState::Released => cvkg_core::Event::PointerUp { x: self.cursor_pos[0], y: self.cursor_pos[1] },
180                    };
181                    vdom.dispatch_event(event);
182                }
183            }
184            WindowEvent::KeyboardInput { event, .. } => {
185                if let Some(vdom) = &self.vdom {
186                    if let winit::keyboard::PhysicalKey::Code(code) = event.physical_key {
187                        let key_str = format!("{:?}", code);
188                        let cvkg_event = if event.state == winit::event::ElementState::Pressed {
189                            cvkg_core::Event::KeyDown { key: key_str }
190                        } else {
191                            cvkg_core::Event::KeyUp { key: key_str }
192                        };
193                        vdom.dispatch_event(cvkg_event);
194                    }
195                    
196                    // Also handle text input (IME / character events)
197                    if event.state == winit::event::ElementState::Pressed {
198                        if let Some(text) = event.text {
199                            for c in text.chars() {
200                                vdom.dispatch_event(cvkg_core::Event::KeyDown { key: c.to_string() });
201                            }
202                        }
203                    }
204                }
205            }
206            _ => (),
207        }
208    }
209
210    fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
211        if let Some(renderer) = &self.renderer {
212            renderer.window.request_redraw();
213        }
214    }
215}
216
217impl cvkg_core::Renderer for NativeRenderer {
218    fn fill_rect(&mut self, rect: cvkg_core::Rect, color: [f32; 4]) {
219        if let Some(gpu) = &mut self.gpu {
220            gpu.fill_rect(rect, color);
221        }
222    }
223    fn fill_rounded_rect(&mut self, rect: cvkg_core::Rect, radius: f32, color: [f32; 4]) {
224        if let Some(gpu) = &mut self.gpu {
225            gpu.fill_rounded_rect(rect, radius, color);
226        }
227    }
228    fn fill_ellipse(&mut self, rect: cvkg_core::Rect, color: [f32; 4]) {
229        if let Some(gpu) = &mut self.gpu {
230            gpu.fill_ellipse(rect, color);
231        }
232    }
233    fn stroke_rect(&mut self, rect: cvkg_core::Rect, color: [f32; 4], stroke_width: f32) {
234        if let Some(gpu) = &mut self.gpu {
235            gpu.stroke_rect(rect, color, stroke_width);
236        }
237    }
238    fn stroke_rounded_rect(&mut self, rect: cvkg_core::Rect, radius: f32, color: [f32; 4], stroke_width: f32) {
239        if let Some(gpu) = &mut self.gpu {
240            gpu.stroke_rounded_rect(rect, radius, color, stroke_width);
241        }
242    }
243    fn stroke_ellipse(&mut self, rect: cvkg_core::Rect, color: [f32; 4], stroke_width: f32) {
244        if let Some(gpu) = &mut self.gpu {
245            gpu.stroke_ellipse(rect, color, stroke_width);
246        }
247    }
248    fn draw_line(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, color: [f32; 4], stroke_width: f32) {
249        if let Some(gpu) = &mut self.gpu {
250            gpu.draw_line(x1, y1, x2, y2, color, stroke_width);
251        }
252    }
253    fn draw_text(&mut self, text: &str, x: f32, y: f32, size: f32, color: [f32; 4]) {
254        if let Some(gpu) = &mut self.gpu {
255            gpu.draw_text(text, x, y, size, color);
256        }
257    }
258    fn measure_text(&mut self, text: &str, size: f32) -> (f32, f32) {
259        if let Some(gpu) = &mut self.gpu {
260            gpu.measure_text(text, size)
261        } else {
262            (0.0, 0.0)
263        }
264    }
265    fn draw_texture(&mut self, texture_id: u32, rect: cvkg_core::Rect) {
266        if let Some(gpu) = &mut self.gpu {
267            gpu.draw_texture(texture_id, rect);
268        }
269    }
270    fn draw_image(&mut self, image_name: &str, rect: cvkg_core::Rect) {
271        if let Some(gpu) = &mut self.gpu {
272            gpu.draw_image(image_name, rect);
273        }
274    }
275    fn load_image(&mut self, name: &str, data: &[u8]) {
276        if let Some(gpu) = &mut self.gpu {
277            gpu.load_image(name, data);
278        }
279    }
280    fn push_clip_rect(&mut self, rect: cvkg_core::Rect) {
281        if let Some(gpu) = &mut self.gpu {
282            gpu.push_clip_rect(rect);
283        }
284    }
285    fn pop_clip_rect(&mut self) {
286        if let Some(gpu) = &mut self.gpu {
287            gpu.pop_clip_rect();
288        }
289    }
290    fn push_opacity(&mut self, opacity: f32) {
291        if let Some(gpu) = &mut self.gpu {
292            gpu.push_opacity(opacity);
293        }
294    }
295    fn pop_opacity(&mut self) {
296        if let Some(gpu) = &mut self.gpu {
297            gpu.pop_opacity();
298        }
299    }
300    fn bifrost(&mut self, rect: cvkg_core::Rect, blur: f32, saturation: f32, opacity: f32) {
301        if let Some(gpu) = &mut self.gpu {
302            gpu.bifrost(rect, blur, saturation, opacity);
303        }
304    }
305    fn push_mjolnir_slice(&mut self, angle: f32, offset: f32) {
306        if let Some(gpu) = &mut self.gpu {
307            gpu.push_mjolnir_slice(angle, offset);
308        }
309    }
310    fn pop_mjolnir_slice(&mut self) {
311        if let Some(gpu) = &mut self.gpu {
312            gpu.pop_mjolnir_slice();
313        }
314    }
315    fn register_shared_element(&mut self, id: &str, rect: cvkg_core::Rect) {
316        if let Some(gpu) = &mut self.gpu {
317            gpu.register_shared_element(id, rect);
318        }
319    }
320}
321
322impl FrameRenderer<()> for NativeRenderer {
323    fn begin_frame(&mut self) -> () { () }
324    fn end_frame(&mut self, _encoder: ()) {}
325}
326
327// Platform-specific implementations for macOS, Windows, and Linux are handled by winit and AccessKit.
328
329struct ShieldWall;
330
331impl accesskit::ActionHandler for ShieldWall {
332    fn do_action(&mut self, _request: accesskit::ActionRequest) {}
333}
334
335impl accesskit::ActivationHandler for ShieldWall {
336    fn request_initial_tree(&mut self) -> Option<accesskit::TreeUpdate> {
337        let mut root = accesskit::Node::new(accesskit::Role::Window);
338        root.set_label("CVKG Application");
339        
340        let root_id = accesskit::NodeId(1);
341        Some(accesskit::TreeUpdate {
342            nodes: vec![(root_id, root)],
343            tree: Some(accesskit::Tree::new(root_id)),
344            focus: root_id,
345        })
346    }
347}
348
349impl accesskit::DeactivationHandler for ShieldWall {
350    fn deactivate_accessibility(&mut self) {}
351}
352
353/// A concrete AssetManager for native desktop targets that loads from the local filesystem.
354pub struct NativeAssetManager {
355    cache: std::sync::Arc<std::sync::RwLock<std::collections::HashMap<String, cvkg_core::AssetState<std::sync::Arc<Vec<u8>>>>>>,
356}
357
358impl NativeAssetManager {
359    pub fn new() -> Self {
360        Self {
361            cache: std::sync::Arc::new(std::sync::RwLock::new(std::collections::HashMap::new())),
362        }
363    }
364}
365
366impl cvkg_core::AssetManager for NativeAssetManager {
367    fn load_image(&self, url: &str) -> cvkg_core::AssetState<std::sync::Arc<Vec<u8>>> {
368        {
369            let cache = self.cache.read().unwrap();
370            if let Some(state) = cache.get(url) {
371                return state.clone();
372            }
373        }
374
375        // Real filesystem I/O (simplistic implementation for now)
376        match std::fs::read(url) {
377            Ok(data) => {
378                let state = cvkg_core::AssetState::Ready(std::sync::Arc::new(data));
379                let mut cache = self.cache.write().unwrap();
380                cache.insert(url.to_string(), state.clone());
381                state
382            }
383            Err(e) => {
384                let state = cvkg_core::AssetState::Error(e.to_string());
385                let mut cache = self.cache.write().unwrap();
386                cache.insert(url.to_string(), state.clone());
387                state
388            }
389        }
390    }
391
392    fn preload_image(&self, _url: &str) {
393        // Implementation for async preloading could go here
394    }
395}